Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: error when script-shell is configured to a .bat or .cmd file #7945

Merged
merged 2 commits into from
Apr 18, 2024

Conversation

gluxon
Copy link
Member

@gluxon gluxon commented Apr 17, 2024

Problem

Configuring script-shell in .npmrc on Windows to a .bat or .cmd file,

# .npmrc
script-shell = node_modules/.bin/shell-mock.cmd

will result in:

spawn EINVAL

  at spawn (../../node_modules/.pnpm/@pnpm+npm-lifecycle@3.0.2_typanion@3.14.0/node_modules/@pnpm/npm-lifecycle/lib/spawn.js:36:15)

This previously worked, but changed after the Node.js security release on April 10th, 2024.

Prior Attempt

Yesterday, I attempted to update @pnpm/npm-lifecycle to support .bat/.cmd files, but this change was reverted after discovering behavior differences with arguments passed to these .bat/.cmd files.

pnpm/npm-lifecycle#42

What does npm do?

As of version 10.5.0, npm doesn't handle .cmd files for script-shell either on Node.js 21.7.3.

Screenshot 2024-04-16 at 10 45 18 PM

Proposal

Let's show an error if .bat/.cmd files are set as the script-shell for now.

I think there's several strong reasons for this:

  1. We would have to implement a Windows-specific argument escaping function to maintain expected behavior under this configuration.
  2. npm doesn't support this configuration.
  3. This behavior is currently broken when pnpm runs on newer versions of Node.js anyway, so this change just makes the existing error more clear. We can always try a different approach later if users provide a valid use-case.

To elaborate on the first point, I haven't found an easy way to support .bat/.cmd files as shells on newer versions of Node.js without making .bat or .cmd files behave differently than .exe files or shells on Linux or macOS. Arguments are parsed regardless of spawn, exec, execFile, or windowsVerbatimArguments options. I believe the correct behavior would involve manually escaping variables like %PATH% so they're passed verbatim to the script-shell. The Rust team attempted to do something similar so arguments are passed verbatim, but their implementation was incorrect. I don't think we should try to do the same given that. From their blog post (emphasis mine):

On Windows, the implementation of this is more complex than other platforms, because the Windows API only provides a single string containing all the arguments to the spawned process, and it's up to the spawned process to split them. Most programs use the standard C run-time argv, which in practice results in a mostly consistent way arguments are splitted.

One exception though is cmd.exe (used among other things to execute batch files), which has its own argument splitting logic. That forces the standard library to implement custom escaping for arguments passed to batch files. Unfortunately it was reported that our escaping logic was not thorough enough, and it was possible to pass malicious arguments that would result in arbitrary shell execution.

Changes

Here's a screenshot of the new error users will see under this unsupported configuration.

Screenshot 2024-04-16 at 11 06 50 PM

// A .exe file to configure as the scriptShell option would be necessary to test
// this behavior on Windows. Skip this test for now since compiling a custom
// .exe would be quite involved and hard to maintain.
skipOnWindows('pnpm run with custom shell', async () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

Note that we're no longer testing custom script-shell setups on Windows on CI. This has been broken for the last week due to this issue.

Screenshot 2024-04-16 at 11 13 01 PM

Copy link
Member Author

@gluxon gluxon Apr 17, 2024

Choose a reason for hiding this comment

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

If we do want to support this test, I can compile a .exe file to @pnpm.e2e/shell-mock that matches the behavior of shell-mock.js. Would that be useful, or overkill?

(I actually think we shouldn't do this since it's hard to audit .exe files on CI and it would run locally for pnpm contributors using Windows.)

@gluxon gluxon marked this pull request as ready for review April 17, 2024 04:09
@gluxon gluxon requested a review from zkochan as a code owner April 17, 2024 04:09
@gluxon gluxon requested a review from KSXGitHub April 17, 2024 04:35
@@ -441,3 +443,14 @@ Run ${chalk.yellow('pnpm dedupe')} to apply the changes above.
`,
}
}

function reportInvalidScriptShellWindowsError (_err: unknown): ErrorInfo {
Copy link
Contributor

Choose a reason for hiding this comment

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

_err isn't used. Why declare it?

Copy link
Member Author

Choose a reason for hiding this comment

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

Intention was to be explicit that the original error is unused and a new one with a more detailed explanation is created.

Happy to just remove the argument if you think that's clearer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, it's kinda subjective. I do personally think that not passing the error as function argument would communicate the intention of ignoring the error object in a more explicit manner.

Copy link
Member Author

@gluxon gluxon Apr 17, 2024

Choose a reason for hiding this comment

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

Appreciate the thoughts nonetheless! I think I'm going to inline the hint instead of throwing a generic error and adding pnpm-specific reporting afterward, which should remove the changes in the cli/default-reporter package.

throw new PnpmError('ERR_PNPM_INVALID_SCRIPT_SHELL_WINDOWS', 'Cannot spawn .bat or .cmd as a script shell.', {
hint: `\
The .npmrc script-shell option was configured to a .bat or .cmd file. These cannot be used as a script shell reliably.
Please unset the script-shell option, or configure it to a .exe instead.
`,
})

I originally didn't want to assume runLifecycleHook's scriptShell option was always configured from .npmrc, but the changes in this PR can be a lot simpler under that assumption.

@gluxon gluxon force-pushed the show-error-if-executing-cmd-or-bat branch from 1ecde86 to 6d71b29 Compare April 17, 2024 16:40
@zkochan zkochan merged commit bfadc0a into main Apr 18, 2024
14 checks passed
@zkochan zkochan deleted the show-error-if-executing-cmd-or-bat branch April 18, 2024 15:34
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.

3 participants