You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Project:angular/dev-infra (bazel/rules/rules_angular), also mirrored to angular/rules_angular and consumed by angular/angular via git_override Commit tested:6a54b1f6 (main) Affected files:bazel/rules/rules_angular/src/ng_package/angular_package_format.bzl, bazel/rules/rules_angular/src/ng_package/rollup/rollup.config.js Type: Code injection into a generated build script, executed during the Bazel build Prior remediation: PR #3732 / commits 9eff3b73, 7fd1ad87 — titled "prevent RCE via argument injection in rollup template". That fix is ineffective; this report bypasses it.
Description
_write_rollup_config passes nine substitutions to ctx.actions.expand_template. Bazel applies them sequentially over the accumulating template — LocalTemplateExpansionStrategy.getExpandedTemplateUnsafe loops replaceAllLiteral and reassigns the result — so text inserted by one substitution is re-scanned by every later one.
json.encode escapes quotes inside a value, but has no reason to escape the literal text TMPL_side_effect_entrypoints; it is plain [A-Za-z_]. An externals entry containing that text therefore survives encoding into the emitted string literal, and the next substitution splices a raw JSON array — with unescaped " — into the middle of it, terminating the literal and injecting JavaScript into the generated config.
Rollup executes that config when loading --config, so the injected code runs as part of the build.
This defeats the premise of the #3732 fix. The reviewing bot on that PR asserted that json.encode "already produces a perfectly valid and safe JavaScript array literal … completely safe from injection". That holds only for a single-pass substitution; expand_template is not single-pass.
Two further sinks introduced by the same code were never converted and remain directly injectable:
constentrypointMetadata=JSON.parse(`TMPL_metadata`);// template literal: ${…} evaluatesconstsideEffectEntryPoints=JSON.parse('TMPL_side_effect_entrypoints');// single-quoted: ' breaks out
Reproduction
Append to bazel/rules/rules_angular/test/ng_package_malicious/BUILD.bazel:
The payload is quote- and backtick-free (String.fromCharCode) so it stays valid in all three templated contexts. It runs touch /tmp/rules_angular_rce_poc.
cd bazel/rules/rules_angular
rm -f /tmp/rules_angular_rce_poc
pnpm bazel build //test/ng_package_malicious:bypass_pkg
ls -la /tmp/rules_angular_rce_poc
There is no error and no warning. Execution is silent and the build result is green.
Expected result
The externals value stays inert: "aTMPL_side_effect_entrypointsb".
Why the existing regression target does not catch this
test/ng_package_malicious (added by #3732 as the regression target for this exact class) is a build-only smoke target with no assertion on the generated config, so it passes whether or not injection occurs.
Separately, a payload containing quotes produces a syntax error rather than execution — the naive form breaks the build loudly. Only a quote-free payload is silent, which is why this was not noticed incidentally.
Root cause, isolated
A two-entry template with no Angular dependencies reproduces the primitive on Bazel 8.7.0:
The forward reference is spliced; the backward one is not. Starlark dict insertion order is the substitution order. Any Bazel rule that passes multiple substitutions to expand_template with values derived from rule inputs has this property — json.encode provides no protection against it.
Suggested fix
Collapse to a single substitution carrying one JSON object, destructured in the template. With one substitution there is no later pass, so no inserted value can be re-entered as a placeholder:
Alternatively, keep rule inputs out of the template entirely by writing them with ctx.actions.write to a sidecar JSON file read at runtime — a pattern already used in six other places in this repository.
Patch submitted as a pull request against this issue.
Verified fix
Applying the change above to angular/dev-infra@6a54b1f6 blocks the payload: the same target builds green and the injected value becomes an inert literal, "aTMPL_side_effect_entrypointsb". The rules_angular test suite (bazel test //..., 9 test targets) still passes with it applied.
Status
rollup.config.js and angular_package_format.bzl on angular/dev-infra@main are still the pre-fix form; last touch is 8befcbf7 (2026-07-20), an unrelated dependency bump.
The angular/rules_angular mirror is identical and equally unfixed.
No matching open or closed issue, no PR, and no published security advisory.
angular/dev-infra@6a54b1f6, Bazel 8.7.0 (pinned by .bazelversion), Node v24.20.0 toolchain / Node v26.7.0 host, macOS arm64. pnpm is not installed on the test machine, so Bazel was invoked as npx @bazel/bazelisk@1.28.1 build …; pnpm bazel is the repository's wrapper around the same binary, and the reproduction steps use that form because it is the documented workflow.
Project:
angular/dev-infra(bazel/rules/rules_angular), also mirrored toangular/rules_angularand consumed byangular/angularviagit_overrideCommit tested:
6a54b1f6(main)Affected files:
bazel/rules/rules_angular/src/ng_package/angular_package_format.bzl,bazel/rules/rules_angular/src/ng_package/rollup/rollup.config.jsType: Code injection into a generated build script, executed during the Bazel build
Prior remediation: PR #3732 / commits
9eff3b73,7fd1ad87— titled "prevent RCE via argument injection in rollup template". That fix is ineffective; this report bypasses it.Description
_write_rollup_configpasses nine substitutions toctx.actions.expand_template. Bazel applies them sequentially over the accumulating template —LocalTemplateExpansionStrategy.getExpandedTemplateUnsafeloopsreplaceAllLiteraland reassigns the result — so text inserted by one substitution is re-scanned by every later one.json.encodeescapes quotes inside a value, but has no reason to escape the literal textTMPL_side_effect_entrypoints; it is plain[A-Za-z_]. Anexternalsentry containing that text therefore survives encoding into the emitted string literal, and the next substitution splices a raw JSON array — with unescaped"— into the middle of it, terminating the literal and injecting JavaScript into the generated config.Rollup executes that config when loading
--config, so the injected code runs as part of the build.This defeats the premise of the #3732 fix. The reviewing bot on that PR asserted that
json.encode"already produces a perfectly valid and safe JavaScript array literal … completely safe from injection". That holds only for a single-pass substitution;expand_templateis not single-pass.Two further sinks introduced by the same code were never converted and remain directly injectable:
Reproduction
Append to
bazel/rules/rules_angular/test/ng_package_malicious/BUILD.bazel:The payload is quote- and backtick-free (
String.fromCharCode) so it stays valid in all three templated contexts. It runstouch /tmp/rules_angular_rce_poc.Actual result
The build succeeds:
and the payload has run:
Line 70 of
bazel-out/.../test/ng_package_malicious/_bypass_pkg_fesm.rollup.conf.js, with theWELL_KNOWN_EXTERNALSentries elided:There is no error and no warning. Execution is silent and the build result is green.
Expected result
The
externalsvalue stays inert:"aTMPL_side_effect_entrypointsb".Why the existing regression target does not catch this
test/ng_package_malicious(added by #3732 as the regression target for this exact class) is a build-only smoke target with no assertion on the generated config, so it passes whether or not injection occurs.Separately, a payload containing quotes produces a syntax error rather than execution — the naive form breaks the build loudly. Only a quote-free payload is silent, which is why this was not noticed incidentally.
Root cause, isolated
A two-entry template with no Angular dependencies reproduces the primitive on Bazel 8.7.0:
The forward reference is spliced; the backward one is not. Starlark dict insertion order is the substitution order. Any Bazel rule that passes multiple substitutions to
expand_templatewith values derived from rule inputs has this property —json.encodeprovides no protection against it.Suggested fix
Collapse to a single substitution carrying one JSON object, destructured in the template. With one substitution there is no later pass, so no inserted value can be re-entered as a placeholder:
Alternatively, keep rule inputs out of the template entirely by writing them with
ctx.actions.writeto a sidecar JSON file read at runtime — a pattern already used in six other places in this repository.Patch submitted as a pull request against this issue.
Verified fix
Applying the change above to
angular/dev-infra@6a54b1f6blocks the payload: the same target builds green and the injected value becomes an inert literal,"aTMPL_side_effect_entrypointsb". Therules_angulartest suite (bazel test //..., 9 test targets) still passes with it applied.Status
rollup.config.jsandangular_package_format.bzlonangular/dev-infra@mainare still the pre-fix form; last touch is8befcbf7(2026-07-20), an unrelated dependency bump.angular/rules_angularmirror is identical and equally unfixed.Environment
angular/dev-infra@6a54b1f6, Bazel 8.7.0 (pinned by.bazelversion), Node v24.20.0 toolchain / Node v26.7.0 host, macOS arm64.pnpmis not installed on the test machine, so Bazel was invoked asnpx @bazel/bazelisk@1.28.1 build …;pnpm bazelis the repository's wrapper around the same binary, and the reproduction steps use that form because it is the documented workflow.