n8n 封锁环境变量后,Code 和表达式还能读取吗?40 次边界对照
两个网络关闭的 n8n 2.30.5 容器共执行 40 次。显式封锁时 Code 与表达式 20/20 被拒绝且哨兵泄露为 0;显式开放时两者 20/20 读到同一哨兵值。
PUBLIC EVIDENCE PACKAGE
不要只读结论,下载原始记录。
环境变量不是天然不可见
把 API 密钥写进容器环境变量,只解决了“不要把值硬编码进工作流 JSON”的问题。工作流作者能否通过 Code 节点或普通表达式读取这些值,是另一个权限边界。
我们固定 n8n 2.30.5,用同一个无敏感性的值 PUBLIC_SENTINEL_20260724 做对照。一个容器显式设置 N8N_BLOCK_ENV_ACCESS_IN_NODE=true,另一个显式设为 false。两个容器都关闭网络,避免工作流把读取结果发送到外部。
40 次结果没有灰区:封锁组 20 次全部被拒绝,哨兵返回 0 次;开放组 20 次全部执行成功,哨兵返回 20 次。
两个入口分别测试,不拿一个代表全部
实验公开两份可导入工作流。第一份在 Code 节点里读取 $env.EXPERIMENT_SENTINEL;第二份在 Set 节点的字段表达式中读取同一个值。每个入口在封锁和开放模式下各执行十次。
| 模式 | 入口 | 执行成功 | 读到哨兵 | 拒绝访问 |
|---|---|---|---|---|
| 显式封锁 | Code 节点 | 0 / 10 | 0 / 10 | 10 / 10 |
| 显式封锁 | 表达式 | 0 / 10 | 0 / 10 | 10 / 10 |
| 显式开放 | Code 节点 | 10 / 10 | 10 / 10 | 0 / 10 |
| 显式开放 | 表达式 | 10 / 10 | 10 / 10 | 0 / 10 |
每种模式使用一个新容器和独立数据卷。镜像固定为 n8nio/n8n:2.30.5 及公开摘要,Docker Server 为 28.3.3。runs.csv 保留每次结构化状态、退出码、哨兵布尔值、错误分类、耗时和原始输出哈希。
封锁模式确实阻止了值进入输出
表达式入口在十次封锁运行中都返回 error,CLI 退出码为 1,结构化错误包含 access to env vars denied。没有一次输出包含哨兵。
Code 节点也是十次 error、十次非零退出、零次哨兵。它在这个版本中把相同拒绝原因包进另一条只读 Error 属性异常。我们没有用完整错误字符串做唯一判定,而是同时检查四项:执行必须为 error、退出码不能为 0、输出不能等于哨兵、拒绝原因必须能从结构化错误中分类。
这项差异说明告警规则不应依赖一条逐字匹配的完整报错。相同安全边界从不同执行入口冒出来,外层错误文本可能不同;结构化状态和核心拒绝原因更稳定。
开放一次,就同时开放两个工作流入口
N8N_BLOCK_ENV_ACCESS_IN_NODE=false 的容器里,Code 节点十次都返回哨兵,表达式也十次返回。两者执行状态全是 success,退出码全为 0。
这里没有利用漏洞,也没有绕过节点配置。开放模式本来就允许工作流通过 $env 读取容器环境。重要的是权限范围:它不是只给某一条受信任表达式开放,而是改变实例中相关工作流入口的访问边界。
如果同一实例允许多个团队编辑工作流,同时把数据库口令、云服务密钥和内部令牌都注入主容器,显式开放会扩大这些值的可读面。即使编辑器不直接显示环境变量,能运行 Code 或表达式的工作流仍可能把值写入执行数据、日志或 HTTP 请求。
我会怎样配置生产实例
不需要工作流读取宿主环境时,显式保持封锁,并把它写进部署清单与升级回归。显式配置比依赖版本默认值更容易审计;本实验也没有推断其他 n8n 版本或托管产品的默认设置。
确实需要动态配置时,先区分“普通配置”和“秘密”。时区、公开端点或功能开关可以通过受控配置进入工作流;密钥应优先放在 n8n 凭据或正式的外部秘密集成中,并把工作流编辑、运行和凭据使用权限分开。
主进程、worker 和 task runner 的配置需要一致检查。只改编辑器容器却漏掉执行工作流的 worker,会让测试环境与生产执行路径不同。升级后至少复跑一个封锁用例和一个允许用例,并确认执行数据没有保存秘密明文。
n8n 的官方安全审计还会检查官方高风险节点、自定义节点、文件系统访问和未保护 Webhook。环境变量封锁只是其中一条边界,不能替代节点白名单、最小权限、日志脱敏、执行数据保留策略和网络出口控制。
为什么实验只使用公开哨兵
安全测试不需要先制造真实泄漏。把生产密钥注入实验容器,再证明工作流能读到它,只会增加新的暴露面。因此协议使用一个写在公开清单里的固定字符串,判断条件只记录“是否匹配”,逐次 CSV 不保存其他环境变量。
两个容器都使用 network none。这样即使开放组成功读取,也无法通过 HTTP 发走。工作流输出里出现公开哨兵是预期观测,不是事故。
公开证据包中的 samples.json 保留四种组合的首个规范化样本。开放组只记录已观察到哨兵的布尔值;封锁组保留拒绝分类和错误文本,用于解释 Code 与表达式外层错误的差异。
这组结果不能证明什么
- 没有向容器放入凭据、生产密钥或其他环境变量,也没有遍历进程环境;
- 没有测试 n8n Cloud、其他版本、队列 worker、外部 task runner 或自定义节点;
- 没有测试外部秘密管理、n8n 凭据对象或不同用户角色的授权;
- 网络关闭,所以没有测量数据外传路径;
- 每种组合十次用于确认固定边界,不用于估计攻击概率。
这项实验能支持的结论很窄:在这两个显式配置的 n8n 2.30.5 容器里,封锁设置同时阻止 Code 与表达式读取哨兵;开放设置同时允许两者读取。是否应该开放,必须根据实例上的工作流作者、执行组件和环境内容做权限判断。
来源与核验日期
- AI Brief Note 原始实验摘要:40 次执行、四种组合、哨兵观测、拒绝次数、镜像摘要和限制。 · 已核验 · generated_from_40_workflow_runs_2026-07-24
- n8n 官方安全环境变量文档:N8N_BLOCK_ENV_ACCESS_IN_NODE 控制节点与表达式的环境变量访问。 · 已核验 · official_site_checked_2026-07-24
- n8n 官方内置变量文档:工作流表达式中的 $env 访问入口。 · 已核验 · official_site_checked_2026-07-24
- n8n 官方安全审计文档:凭据、文件系统、节点与实例级风险检查范围。 · 已核验 · official_site_checked_2026-07-24