Skip to content

Re-home external-editor & fix vuln#1804

Merged
SBoudrias merged 2 commits intomainfrom
external-editor
Aug 8, 2025
Merged

Re-home external-editor & fix vuln#1804
SBoudrias merged 2 commits intomainfrom
external-editor

Conversation

@SBoudrias
Copy link
Copy Markdown
Owner

@SBoudrias SBoudrias commented Aug 8, 2025

Rel #1802

TODOs

  • Confirm backward compatibility (so it's easy for dependents to move over to a drop-in replacement)

Follow-ups (other PR)

  • Align error handling with the rest Inquirer project (e.g instanceof is error prone)
  • Get rid of the cruft
  • Review class base interface to be more modern

Comment thread packages/editor/package.json Outdated
Comment thread packages/external-editor/README.md
Comment thread packages/external-editor/example_async.js Outdated
Comment thread packages/external-editor/src/errors/CreateFileError.ts
@SBoudrias
Copy link
Copy Markdown
Owner Author

For the curious, GPT-5 prompt used to generate this version:

I want to move all the code from the node-external-editor workspace into a package inside the Inquirer.js workspace monorepo.

Start by simply copy-pasting the code inside a new package folder and do a git commit.

Once that is done, start editing the new package content so that it works in the repository setup. Make sure to run `yarn setup` to ensure the right configs are set. Rename the package to `@inquirer/external-editor`. And delete the redundant config files.

Finally, run linting (yarn pretest) and test (yarn test), and fix any errors.

I think it one-shotted it (cursory review still). With a little follow-up to get rid of tmp.

Comment thread packages/external-editor/src/index.ts Fixed
Comment thread packages/external-editor/src/index.ts Fixed
@SBoudrias SBoudrias force-pushed the external-editor branch 2 times, most recently from 71dc66b to bf3fddc Compare August 8, 2025 20:47
const prefix = sanitizeAffix(this.fileOptions.prefix);
const postfix = sanitizeAffix(this.fileOptions.postfix);
const filename = `${prefix}${id}${postfix}`;
const candidate = path.resolve(baseDir, filename);

Check warning

Code scanning / CodeQL

Unsafe shell command constructed from library input Medium

This path concatenation which depends on
library input
is later used in a
shell command
.
This path concatenation which depends on
library input
is later used in a
shell command
.
This path concatenation which depends on
library input
is later used in a
shell command
.
This path concatenation which depends on
library input
is later used in a
shell command
.
This path concatenation which depends on
library input
is later used in a
shell command
.
This path concatenation which depends on
library input
is later used in a
shell command
.
const prefix = sanitizeAffix(this.fileOptions.prefix);
const postfix = sanitizeAffix(this.fileOptions.postfix);
const filename = `${prefix}${id}${postfix}`;
const candidate = path.resolve(baseDir, filename);

Check warning

Code scanning / CodeQL

Unsafe shell command constructed from library input Medium

This path concatenation which depends on
library input
is later used in a
shell command
.
This path concatenation which depends on
library input
is later used in a
shell command
.
This path concatenation which depends on
library input
is later used in a
shell command
.
This path concatenation which depends on
library input
is later used in a
shell command
.
This path concatenation which depends on
library input
is later used in a
shell command
.
This path concatenation which depends on
library input
is later used in a
shell command
.

Copilot Autofix

AI 9 months ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

Comment on lines +208 to +212
const editorProcess = spawnSync(
this.editor.bin,
this.editor.args.concat([this.tempFile]),
{ stdio: 'inherit' },
);

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium

This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.

Copilot Autofix

AI 9 months ago

To fix the problem, we should ensure that the editor process is not spawned via a shell, and that the binary and arguments are not interpreted by the shell. This is achieved by explicitly setting the shell option to false in both spawnSync and spawn calls. Additionally, we should validate the editor binary to ensure it is a simple executable name or absolute path, and not a shell command or script with special characters. The changes should be made in the launchEditor and launchEditorAsync methods in packages/external-editor/src/index.ts. No changes are needed in the example file.

Required changes:

  • In launchEditor and launchEditorAsync, add shell: false to the options object.
  • Optionally, add a simple validation for this.editor.bin to ensure it does not contain spaces or shell metacharacters (e.g., ;, |, &, >, <, `, $, etc.).
  • No new imports are required.

Suggested changeset 1
packages/external-editor/src/index.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/external-editor/src/index.ts b/packages/external-editor/src/index.ts
--- a/packages/external-editor/src/index.ts
+++ b/packages/external-editor/src/index.ts
@@ -207,2 +207,6 @@
     try {
+      // Validate editor binary for safety
+      if (/[;&|><`$]/.test(this.editor.bin)) {
+        throw new LaunchEditorError(new Error('Unsafe editor binary detected'));
+      }
       const editorProcess = spawnSync(
@@ -210,3 +214,3 @@
         this.editor.args.concat([this.tempFile]),
-        { stdio: 'inherit' },
+        { stdio: 'inherit', shell: false },
       );
@@ -220,2 +224,6 @@
     try {
+      // Validate editor binary for safety
+      if (/[;&|><`$]/.test(this.editor.bin)) {
+        throw new LaunchEditorError(new Error('Unsafe editor binary detected'));
+      }
       const editorProcess = spawn(
@@ -223,3 +231,3 @@
         this.editor.args.concat([this.tempFile]),
-        { stdio: 'inherit' },
+        { stdio: 'inherit', shell: false },
       );
EOF
@@ -207,2 +207,6 @@
try {
// Validate editor binary for safety
if (/[;&|><`$]/.test(this.editor.bin)) {
throw new LaunchEditorError(new Error('Unsafe editor binary detected'));
}
const editorProcess = spawnSync(
@@ -210,3 +214,3 @@
this.editor.args.concat([this.tempFile]),
{ stdio: 'inherit' },
{ stdio: 'inherit', shell: false },
);
@@ -220,2 +224,6 @@
try {
// Validate editor binary for safety
if (/[;&|><`$]/.test(this.editor.bin)) {
throw new LaunchEditorError(new Error('Unsafe editor binary detected'));
}
const editorProcess = spawn(
@@ -223,3 +231,3 @@
this.editor.args.concat([this.tempFile]),
{ stdio: 'inherit' },
{ stdio: 'inherit', shell: false },
);
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +221 to +225
const editorProcess = spawn(
this.editor.bin,
this.editor.args.concat([this.tempFile]),
{ stdio: 'inherit' },
);

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium

This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.

Copilot Autofix

AI 9 months ago

To fix the problem, we should ensure that the editor binary and its arguments are always passed as separate parameters to spawn and spawnSync, and never as a single string that could be interpreted by a shell. Additionally, we should sanitize the values obtained from environment variables (EDITOR, VISUAL) to prevent them from being shell commands or containing unsafe characters. The best way to do this is to parse the editor command into a binary and arguments, and reject or sanitize any values that appear to be shell commands (e.g., containing ;, &&, |, or starting with sh -c). We should also ensure that the spawn and spawnSync calls do not use shell: true.

The main changes should be made in the determineEditor method in packages/external-editor/src/index.ts, where the editor command is parsed from environment variables. We should add validation to reject unsafe values and ensure that only a binary and arguments are used. If an unsafe value is detected, we should fall back to a safe default editor.


Suggested changeset 1
packages/external-editor/src/index.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/external-editor/src/index.ts b/packages/external-editor/src/index.ts
--- a/packages/external-editor/src/index.ts
+++ b/packages/external-editor/src/index.ts
@@ -138,3 +138,3 @@
   private determineEditor() {
-    const editor = process.env['VISUAL']
+    let editor = process.env['VISUAL']
       ? process.env['VISUAL']
@@ -146,2 +146,9 @@
 
+    // Sanitize editor command: reject if it contains shell metacharacters
+    const unsafePattern = /[;&|`$><]/;
+    if (unsafePattern.test(editor)) {
+      // Fallback to safe default
+      editor = process.platform.startsWith('win') ? 'notepad' : 'vim';
+    }
+
     const editorOpts = splitStringBySpace(editor).map((piece: string) =>
EOF
@@ -138,3 +138,3 @@
private determineEditor() {
const editor = process.env['VISUAL']
let editor = process.env['VISUAL']
? process.env['VISUAL']
@@ -146,2 +146,9 @@

// Sanitize editor command: reject if it contains shell metacharacters
const unsafePattern = /[;&|`$><]/;
if (unsafePattern.test(editor)) {
// Fallback to safe default
editor = process.platform.startsWith('win') ? 'notepad' : 'vim';
}

const editorOpts = splitStringBySpace(editor).map((piece: string) =>
Copilot is powered by AI and may make mistakes. Always verify output.
@SBoudrias SBoudrias merged commit 9475bb3 into main Aug 8, 2025
12 checks passed
@SBoudrias SBoudrias deleted the external-editor branch August 8, 2025 21:11
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