Skip to content

fix(core): pass release publish commands as argv instead of shell strings - #36874

Open
AgentEnder wants to merge 7 commits into
masterfrom
fix/nxc-4918-publish-command-injection
Open

fix(core): pass release publish commands as argv instead of shell strings#36874
AgentEnder wants to merge 7 commits into
masterfrom
fix/nxc-4918-publish-command-injection

Conversation

@AgentEnder

Copy link
Copy Markdown
Member

Important

Stacked on #36867 — needs a rebase once that merges.
The bottom two commits here are @wangxpych's from #36867, rebased onto current master. They are
included only because this PR rewrites the same pnpm publish block and would otherwise conflict
with it. Once #36867 lands, rebase this branch onto master and those two commits drop out; the
diff is then just the four NXC-4918 commits. Please don't merge this before #36867.

Current Behavior

nx release builds its npm invocations as shell strings and runs them through execSync / exec,
interpolating the registry and tag directly into the command. Both values default to
npm config get registry / npm config get tag, which return the workspace .npmrc value
verbatim, and the tag was interpolated with no quoting at all.

That makes .npmrc an execution vector rather than configuration. It matters because .npmrc is
writable by a dependency's install lifecycle script, and these commands run in CI with publish
credentials present.

The same pair reaches a shell from more than the one place:

Path Command
nx release publish <pm> publish, npm view / bun info, npm dist-tag add
nx release version npm view … version, when currentVersionResolver is registry
both npm config get <scope>:registry, where the scope comes from the package manifest

--otp and --access were interpolated unquoted too.

Expected Behavior

Every one of those commands is passed as argv, so shell syntax in any of the values is data
rather than syntax. Off Windows no shell is involved at all.

  • PackageManagerCommands gains publishArgv, returning { command, args }, for all four package
    managers. The existing publish is kept for external callers, but is now derived from
    publishArgv through quoteShellArg — so the two renderings cannot drift — and is deprecated for
    Nx 24.
  • The publish executor, parseRegistryOptions and readCurrentVersionFromRegistry all move onto
    the existing safeExecFileSync / safeSpawn helpers.
  • quoteShellArg now refuses a line break on Windows, matching the guard safeSpawn already
    applied. A line break ends a cmd.exe command line whatever quoting surrounds it, so this is the
    one input quoting cannot make safe. POSIX is unaffected, since a single-quoted run holds a newline
    fine.

Coverage

Regression tests assert the untrusted values arrive as single argv elements rather than
asserting an emitted string, so they fail if the escaping is removed rather than merely changing.
The string form additionally round-trips through a real shell and asserts the tokens come back equal
to the argv, with NUL delimiting so a newline cannot forge a token boundary. Each new guard was
mutation-tested — removing it fails the corresponding test.

readCurrentVersionFromRegistry had no test at all before this; it has one now.

Not closed by this PR

On Windows npm is a bare name, so safeExecFileSync still goes through cmd.exe, which expands
%VAR% inside quotes. That is the pre-existing, documented gap tracked as NXC-4798, and it now
has .npmrc as one of its inputs. Worth knowing before treating this as a complete fix on Windows.

Related Issue(s)

Internal ticket: NXC-4918. No public issue is linked, so there is nothing for this to auto-close.

wangxpych and others added 6 commits September 1, 2026 00:19
Use pnpm's config-prefixed scoped registry option so explicit release registries override workspace .npmrc settings.\n\nclosed #36860
A line break ends a cmd.exe command line whatever the quoting sits around
it, so a quoted run cannot carry one and everything after it is read as a
fresh command. `quoteShellArg` caught a literal double quote but not this.

- move `LINE_BREAK` into shell-quoting and export it, so the rule has one
  definition; `safe-spawn` now imports it instead of declaring its own
- throw from `quoteShellArg` on Windows only, since a POSIX single-quoted
  run does hold a line break safely

`safeSpawn`'s own `quoteForCmd` check stays as the first guard so its
contextual messages are unchanged.
`publish` returned a shell string into which the registry and tag were
interpolated. Both come back from `npm config get`, i.e. verbatim from a
workspace .npmrc, which an install script can write - so shell syntax in
either one reached the shell that ran the command.

- add `publishArgv`, returning `{ command, args }`, for all four package
  managers
- derive `publish` from it through `quoteShellArg` so the two renderings
  cannot drift, and deprecate it for Nx 24

Off Windows the argv form needs no shell at all. Windows still quotes,
and now fails closed on the arguments quoting cannot make safe.
Every child process the publish executor started went through `execSync`
with its arguments interpolated into the command string. Besides publish
itself, `npm view` and `npm dist-tag add` carried the same registry and
tag, and the latter interpolated the tag with no quoting at all.

- move all four sinks onto `safeExecFileSync`, passing argv
- pass `otp`, `access` and `--dry-run` as argv elements too
- read npm config through `safeSpawn` in `parseRegistryOptions`, so the
  scope taken from the package manifest is an argument rather than part
  of the command string

`safeExecFileSync` fixes the encoding to utf-8, so the error streams are
now strings; the specs track that instead of leaving the mocks wrong.
`readCurrentVersionFromRegistry` interpolated the same registry and tag
into an `exec` command string, so `nx release version` reached a shell
with them on every platform when `currentVersionResolver` is `registry`.

Run it through `safeSpawn` with argv, matching the publish path, and
cover the method - it had no test before.

`cwd` stays unset so npm still reads the same .npmrc `exec` gave it.
@AgentEnder
AgentEnder requested a review from a team as a code owner September 1, 2026 04:28
@netlify

netlify Bot commented Sep 1, 2026

Copy link
Copy Markdown

Deploy Preview for nx-docs ready!

Name Link
🔨 Latest commit d27576a
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/6a9660e572af990008f8bd37
😎 Deploy Preview https://deploy-preview-36874--nx-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Sep 1, 2026

Copy link
Copy Markdown

Deploy Preview for nx-dev ready!

Name Link
🔨 Latest commit d27576a
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/6a9660e55b045b0008debaa5
😎 Deploy Preview https://deploy-preview-36874--nx-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@nx-cloud

nx-cloud Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit d3cbc23

Command Status Duration Result
nx affected --targets=lint,oxlint,test,build,e2... ✅ Succeeded 8m 42s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 4s View ↗
nx-cloud record -- pnpm nx-cloud conformance:check ✅ Succeeded 1m 2s View ↗
nx build workspace-plugin ✅ Succeeded <1s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded 19s View ↗
nx-cloud record -- nx format:check ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2026-09-01 05:36:03 UTC

@nx-cloud nx-cloud Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nx Cloud has identified a flaky task in your failed CI:

🔂 Since the failure was identified as flaky, we triggered a CI rerun by adding an empty commit to this branch.

Nx Cloud View detailed reasoning in Nx Cloud ↗

🔔 Heads up, your workspace has pending recommendations ↗ to auto-apply fixes for similar failures.


🎓 Learn more about Self-Healing CI on nx.dev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants