Skip to content

child_process: validate options.shell and correctly enforce shell invocation in exec/execSync - #54623

Closed
Renegade334 wants to merge 3 commits into
nodejs:mainfrom
Renegade334:child-process-exec-shell
Closed

child_process: validate options.shell and correctly enforce shell invocation in exec/execSync#54623
Renegade334 wants to merge 3 commits into
nodejs:mainfrom
Renegade334:child-process-exec-shell

Conversation

@Renegade334

@Renegade334Renegade334 commented Aug 28, 2024

Copy link
Copy Markdown
Member

The intention of exec and execSync is that a shell is always invoked to run the provided command, as opposed to execFile, which by default executes the target directly and does not invoke a shell. Unlike its counterparts elsewhere in child_process, exec's options.shell is documented as not accepting a boolean, as it's not intended to be possible to disable shell invocation and switch to execFile-like behaviour.

exec's options.shell is currently handled the following way:

  • passed to normalizeExecArgs()
    • if the shell parameter is not a string, it is coerced to boolean true
  • passed to normalizeSpawnArguments()
    • if non-nullish, the parameter is validated as being of type string|boolean
    • if the parameter is truthy at this point, then spawn() runs a shell; if not, it executes the target directly

The intention is clearly to force shell invocation with exec/execSync in all cases, but the current implementation has the following problems:

  • The parameter is not validated at all; instead, if it's not a string, it's silently ignored and coerced to true, which passes downstream validation in normalizeSpawnArguments(). In other words, passing an invalid value to options.shell will never raise a validation error.
  • The logic in the normalize functions combines to yield an edge case, which is when exec/execSync is called with options.shell set to the empty string:
    • the parameter is passed through unchanged by normalizeExecArgs()
    • the parameter passes validation in normalizeSpawnArguments()
    • the parameter is not truthy, so normalizeSpawnArguments() fails to set up the shell
    • the child process is spawned without a shell, with the file target set to the entire command string:
      child_process.exec('./script some-parameter',{shell: ''})// should invoke a shell to run "./script" and pass an argument// instead, attempts to execute a file called "script some-parameter"

This patch makes two simple changes. Firstly, it validates the shell option sent to exec/execSync as a string. Secondly, it coerces all falsy values to boolean true, so will never bypass shell setup. (This makes { shell: '' } equivalent to { shell: undefined }, which matches how it behaves in the other child_process functions.)

@nodejs-github-botnodejs-github-bot added child_process Issues and PRs related to the child_process subsystem. needs-ci PRs that need a full CI run. labels Aug 28, 2024
Comment threadtest/parallel/test-child-process-exec-enforce-shell.js Outdated
Comment threadlib/child_process.js Outdated
@avivkeller

avivkeller commented Aug 28, 2024

Copy link
Copy Markdown
Member

CC @nodejs/child_process


@Renegade334 can you shorten the commit message, it's too long to land. Something like child_process: validate that `shell` is a XYZ

@Renegade334
Renegade334force-pushed the child-process-exec-shell branch from 22673fb to ce02538CompareAugust 28, 2024 23:51
@avivkelleravivkeller added the commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. label Aug 28, 2024
@codecov

codecovBot commented Aug 29, 2024

Copy link
Copy Markdown

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 88.54%. Comparing base (98d4ebc) to head (79e548e).
Report is 157 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #54623 +/- ##
=======================================
Coverage 88.53% 88.54% =======================================
Files 657 657 Lines 190741 190746 +5 Branches 36607 36605 -2 =======================================
+ Hits 168881 168899 +18 + Misses 15036 15028 -8 + Partials 6824 6819 -5 
Files with missing linesCoverage Δ
lib/child_process.js97.74% <100.00%> (+0.01%)⬆️

... and 41 files with indirect coverage changes

Comment threadtest/parallel/test-child-process-exec-enforce-shell.js Outdated
Comment threadtest/parallel/test-child-process-exec-enforce-shell.js Outdated
Comment threadtest/parallel/test-child-process-exec-enforce-shell.js Outdated
@Renegade334
Renegade334force-pushed the child-process-exec-shell branch from ce02538 to 2288c07CompareAugust 29, 2024 07:14
@jasnelljasnell added the request-ci Add this label to start a Jenkins CI on a PR. label Sep 8, 2024
@github-actionsgithub-actionsBot removed the request-ci Add this label to start a Jenkins CI on a PR. label Sep 8, 2024
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@Renegade334
Renegade334force-pushed the child-process-exec-shell branch from 3cf56e3 to f61299eCompareSeptember 8, 2024 15:07
@jasnell

Copy link
Copy Markdown
Member

PR is currently blocked from landing due to unreliable CI. Looks like the OSX runner is jammed up.

@Renegade334

Copy link
Copy Markdown
MemberAuthor

@jasnell could this go back to request-ci, now that OS X is more stable?

- narrow validation type to string (previously de facto not validated)
- ensure empty string is coerced to true
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

child_processIssues and PRs related to the child_process subsystem.commit-queue-squashAdd this label to instruct the Commit Queue to squash all the PR commits into the first one.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@Renegade334@avivkeller@nodejs-github-bot@jasnell