Skip to content

fix: bypass shell entirely for cross-platform Docker commands#64

Merged
Treelovah merged 3 commits intomainfrom
fix/windows-shell-escape
Mar 10, 2026
Merged

fix: bypass shell entirely for cross-platform Docker commands#64
Treelovah merged 3 commits intomainfrom
fix/windows-shell-escape

Conversation

@Treelovah
Copy link
Contributor

@Treelovah Treelovah commented Mar 3, 2026

Summary

The original commit tried to fix Windows by adding cmd.exe escaping to shellEscape(). Pie's review correctly identified that cmd.exe escaping is fundamentally broken — \" doesn't escape quotes in cmd.exe, %% only works in .bat files, and the implementation had a real injection vector.

The right fix is to not use a shell at all. execFileSync with argument arrays bypasses cmd.exe (and bash) entirely — arguments go directly to the process. No shell means no escaping means no injection surface.

docker.ts: Every execSync call replaced with execFileSync argument arrays. The POSIX sleep 2 replaced with Atomics.wait-based sleepSync() that works cross-platform.

env-check.ts: Same treatment. || echo "FAIL" replaced with try/catch. 2>/dev/null replaced by stdio: 'pipe'.

shell.ts: Reverted to POSIX-only with doc comment explaining to use execFileSync for cross-platform.

Closes #63

Test plan

  • npm run build — clean
  • npx vitest run — 410/410 pass
  • Manual: run benchmark on Windows — Docker commands should work without shell quoting issues
  • Manual: run benchmark on macOS/Linux — no regression

…cker command

shellEscape blindly wraps everything in POSIX single quotes. That's
fine on macOS and Linux where execSync spawns /bin/sh. On Windows it
spawns cmd.exe, which doesn't treat single quotes as quoting characters at all — they get passed through literally as part of the argument.

So `docker pull 'ghcr.io/kryptsec/sqli-auth-bypass:latest'` becomes an image reference that literally starts and ends with an apostrophe, and Docker rightfully tells you that's an invalid reference format. Every single docker and docker exec call in the CLI was broken on Windows.

Now shellEscape checks process.platform and uses double-quote wrapping with proper cmd.exe escaping (percent doubling, backslash-escaped double quotes) on win32. POSIX behavior is unchanged.
cc @pi3-code for additional testing on this.
Closes #63
Copy link
Contributor

@pi3-code pi3-code left a comment

Choose a reason for hiding this comment

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

Review: Windows cmd.exe Shell Escaping

Hey, thanks for tackling this — Windows shell quoting is genuinely one of the trickiest areas to get right, and the core idea here (platform-aware escaping) is the right direction. I dug into cmd.exe's parsing behavior and found a few things worth addressing before this ships.


cmd.exe Quote Escaping

The main thing: cmd.exe doesn't recognize \" as an escaped quote the way POSIX shells or the C runtime do. It treats \ as a literal character, so \" actually closes the quoted region from cmd.exe's perspective. The standard cmd.exe convention is "" (doubled quote).

This matters because input containing a double quote could unintentionally break out of the quoting:

Input:    hello" & whoami
Escaped:  "hello\" & whoami"
cmd.exe interprets the \" as closing the quote, then runs & whoami as a separate command.

Swapping to "" would fix this.

A few other cmd.exe quirks to handle

  • %% for percent escaping — this works in .bat/.cmd files but not reliably at the cmd.exe /c command line (which is what Node's execSync uses). The BatBadBut research has a good writeup on safer alternatives.
  • Caret (^) — it's cmd.exe's escape character and can interfere with parsing even inside quotes. Worth escaping as ^^.
  • Newlines — a \n or \r\n in the input would terminate the command line. Probably safest to throw on these since there's no clean way to embed them.
  • Trailing backslashshellEscape('C:\\') would produce "C:\\", and the C runtime parser can misread \" as an escaped quote, leaving the string unterminated. Edge case but worth covering.
  • ! (delayed expansion) — if cmd /V:ON is active, !var! gets expanded. Lower risk but easy to handle.

Test coverage

The existing tests cover the happy path well. A few additional cases that would strengthen confidence: caret ^, trailing backslash, newlines, and a double-quote-plus-metacharacter combo (the real attack surface).

Bigger picture suggestion

cmd.exe escaping is notoriously fragile — you might want to consider using execFileSync/spawnSync with argument arrays for the Windows path, which bypasses cmd.exe entirely and sidesteps the whole escaping problem:

import { execFileSync } from 'child_process';
execFileSync('docker', ['pull', imageName]); // no shell involved

Alternatively, a library like cross-spawn handles all these edge cases.

Also worth noting: some callers in docker.ts and env-check.ts use POSIX-isms like 2>/dev/null and sleep that would need separate fixes for full Windows support — but that can be a follow-up.


Again, appreciate you taking this on — Windows compat is important and this is a solid start. Happy to chat through any of these if it would help!

The original approach tried to make shellEscape() work on Windows by
adding cmd.exe double-quote escaping. Pie's review correctly identified
that cmd.exe escaping is a minefield — \" doesn't actually escape
quotes in cmd.exe, %% only works in .bat files, and there's a real
injection vector with the current implementation.

The right fix is to not use a shell at all. execFileSync with argument
arrays bypasses cmd.exe (and bash) entirely — arguments are passed
directly to the process without any shell interpretation. No shell
means no shell escaping means no injection surface.

docker.ts: every execSync call replaced with execFileSync argument
arrays. The POSIX `sleep 2` call replaced with Atomics.wait-based
sleepSync() that works cross-platform without a shell.

env-check.ts: same treatment. The `|| echo "FAIL"` pattern in
checkTargetReachable replaced with try/catch. The `2>/dev/null`
stderr suppression replaced by stdio: 'pipe'.

shell.ts: reverted to POSIX-only, added doc comment explaining that
execFileSync should be used for cross-platform instead.

All tests updated to mock execFileSync and assert against argument
arrays instead of command strings. 410/410 passing.
@Treelovah Treelovah changed the title fix: Windows shell quoting breaks all Docker commands fix: bypass shell entirely for cross-platform Docker commands Mar 5, 2026
@Treelovah Treelovah merged commit 83b6a4e into main Mar 10, 2026
4 checks passed
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.

docker pull 'ghcr.io/kryptsec/sqli-auth-bypass:latest'

2 participants