Skip to main content
BlogSecurity

Cursor CVE-2026-22708 RCE: Shell Built-ins Beat the Allowlist

CVE-2026-22708: Cursor ran export and typeset without approval, so a poisoned PAGER made an allowlisted git command run code. Fix 2.3, scores and defender steps.

Matthews Wong

Software Engineer

October 202611 min read
Cursor CVE-2026-22708 RCE: Shell Built-ins Beat the Allowlist

Key Takeaway

CVE-2026-22708 let Cursor versions before 2.3 run shell built-ins such as export, typeset and declare without allowlist checks or approval, so a prompt-injected agent could poison variables like PAGER and turn an approved git command into code execution. Allowlists match command names, not shell state, so use a sandbox as the real boundary.

The part of this bug that stayed with me is how ordinary the approved command was. An allowlist entry for git branch is exactly what a careful team writes, and the researchers showed that the entry could be enough, even with an allowlist that was otherwise empty, because the dangerous step happened one command earlier and nobody was asked about it.

I read the GitHub advisory, the NVD record, Pillar Security's write-up, SentinelOne's database entry, Cursor's run modes and sandbox documentation and the git-var manual. I did not reproduce the bug, and this post contains no exploit payload. Everything below is attributed to one of those pages, and where they disagree I say so.

What the CVE record says, and where the sources disagree

The NVD description says that before Cursor 2.3, with the agent in Auto-Run Mode and Allowlist mode enabled, certain shell built-ins could run without appearing in the allowlist and without user approval. Through direct or indirect prompt injection, an attacker could set, modify or remove environment variables that influence trusted commands. The GitHub advisory GHSA-82wg-qcm4-fp2w adds that a non-default configuration is required and credits Dan Lisichkin of Pillar Security.

FieldNVD (nvd@nist.gov)GitHub as CNA (security-advisories@github.com)
ScoreCVSS 3.1 base 9.8 Critical, network, low complexity, no privileges, no interactionCVSS 4.0 base 7.2 High, attack complexity High, attack requirements Present
WeaknessCWE-77, command injectionCWE-15, 74, 77, 78, 94 and 269
AffectedPrior to 2.3Advisory text: 2.2 and earlier
Fixed2.32.3, described as improved terminal command parsing for edge cases
Published2026-01-14January 14, 2026

The two scores are not a contradiction to resolve, they are two scoring systems applied by two parties. If a scanner or a policy of yours says patch anything 9.0 and above, NVD's 9.8 will trip it while GitHub's 7.2 will not. Read the precondition before the number: this only bites a user who enabled the allowlist workflow, and it needs a prompt injection to reach the agent in the first place.

Why an allowlist never saw the dangerous step

An allowlist answers one question: is this command on the list. Pillar's finding is that Cursor's server-side evaluator treated shell built-ins as implicitly trusted. The built-ins it names are export, readonly, unset, typeset, declare and local, and they run in the current shell, so they are not separate programs that a command-name check would flag. They only change state.

That state is read by the next command. Git's own manual describes the order it uses to pick a pager: GIT_PAGER, then the core.pager setting, then PAGER, then a compiled default, usually less. It also says the value is meant to be interpreted by the shell. So a variable that looks like harmless display configuration is, by design, a string handed to a shell. Pillar's example is exactly this: set PAGER silently through a built-in, then wait for a benign command such as git branch to run.

# Git picks its pager in this order (git-var manual):
#   $GIT_PAGER, then core.pager, then $PAGER, then the compiled default (usually less)
# and the manual says the value is "meant to be interpreted by the shell".
# So each of these is a string a shell will run, not a harmless display setting.

git var GIT_PAGER                               # what git would launch right now
git config --show-origin --get-all core.pager   # is it pinned, and in which file?
echo "PAGER=$PAGER"                             # the last fallback in the chain

Pillar says the same class reaches other variables, naming LD_PRELOAD and GIT_ASKPASS, but notes those require more user interaction. The pager is the clean case because approving git branch feels like approving a read-only listing. The approval was never about the thing that ran.

From one click to zero clicks: the variants Pillar describes

Pillar describes four shapes of the same flaw. I list them at the level they are described, because the point for a defender is the class, not the recipe.

  • PAGER hijacking, one click: a built-in sets PAGER, then an allowlisted git command triggers attacker-controlled code instead of showing output.
  • Python warning handler chain, one click: Pillar credits Luke Jahnke of elttam for a chain of environment variables that makes a later Python run lead to Perl and then to code execution.
  • Arbitrary file write through export, zero click: Pillar says the sanitizer did not recognise a here-string combined with output redirection as a file write, which allows content to be appended to a shell startup file for persistence on each new shell.
  • Direct execution through typeset or declare, zero click: Pillar says these abuse zsh parameter expansion flags so that embedded command substitution is evaluated, and the filter detected no explicit command substitution.

The persistence variant is the one I would worry about most, because its damage outlives the session. A write to a startup file such as ~/.zshrc is not undone by updating Cursor, so patching and auditing are two separate jobs, and the audit is in the Monday section.

Timeline and fix: incomplete first, then a parser rule

Pillar's timeline: reported to Cursor on August 11, 2025; Cursor acknowledged the issue in August; in September it acknowledged a systemic issue with two major initiatives underway; and in January 2026 it shipped a fix. The advisory and the NVD record are both dated January 14, 2026, and the fixed version is 2.3.

Pillar also says Cursor's initial sandbox mitigation appeared incomplete, addressing specific trigger paths while leaving the vulnerability class exploitable. The fix it describes is a rule rather than a patch for one payload: explicit user approval for any command the server-side parser cannot classify. That is the right shape, since it turns an unknown into a prompt instead of an allow, but it is a statement from the researchers, and I could not find the fix described in more detail in the advisory beyond improved parsing for edge cases.

What an allowlist can never guarantee

An allowlist is a filter on one layer, and each layer sees something different. The table lists what Cursor's own documentation says about each layer, next to what the layer cannot see.

LayerWhat it decides onWhat it cannot see
AllowlistWhether the action is on your list; listed actions run without approvalThe environment the command inherits, and anything the parser fails to classify
Auto-review classifierA model on the Cursor backend reviews calls that do not use the sandboxCursor's docs say it is not a security boundary and can make mistakes in either direction
SandboxWhere a command runs: file and network access, whatever the command isCommands that need full access bypass it and go to approval instead
Run EverythingNothing; every tool call runs automaticallyEverything

The structural reason the allowlist cannot be a guarantee is that it keys on the command and the attack lives in the context. Even after the fix, Cursor's Auto-review lifecycle documentation says allowlisted shell, Fetch and MCP calls run outside the sandbox. So the allowlist remains the one lane where a command executes with your full privileges, which is why that list should be short and boring. Pillar quotes Cursor calling the allowlist not a security guarantee, and the current Run Modes page frames the Allowlist mode as deterministic behaviour for a small set of trusted repeat actions.

The trade-off: isolation costs you something

Pillar's recommendation is full execution isolation, with sanitisation treated as the weaker control. I agree with the direction, and Cursor's own sandbox write-up gives the motive: per-command approvals decay into approval fatigue, and it reports sandboxed agents stop 40 percent less often than unsandboxed ones. It uses Seatbelt on macOS, and Landlock plus seccomp on Linux. These are Cursor's figures about its own product, not an independent measurement.

The sandbox has an exit. Cursor's documentation says some commands need full system access and bypass it, and that a sandboxed command that fails on a restriction can be rerun outside the sandbox, with the classifier reviewing the rerun. The config also has a type value, insecure_none, that disables the sandbox entirely. A sandbox you can opt out of per command is only as strong as whoever approves the opt-out.

The cost is friction. Network is blocked by default and opened by your network mode and sandbox.json, so the first npm install in a new project asks for a domain. That friction is the control working, but it is real, and teams that feel it tend to answer with Run Everything, which Cursor's table describes as for people who accept the risk and want zero prompts.

What I could not confirm

Three gaps are worth naming. First, I did not run any of this, so I cannot say which variants still work on a given version; the sources only support that 2.3 is the fixed version. Second, Pillar is the discoverer and sells security products, so its framing favours isolation, and its article states no affected or fixed versions and no CVSS. Third, SentinelOne's database page says in its own text that its content is AI-generated and should be verified, so I used it only to cross-check the NVD numbers.

What to do on Monday

Patching is the smallest part of this. Work through these in order.

  1. Check the Cursor version on every machine and update anything before 2.3. Your config is not safe merely because you never enabled the allowlist; confirm what Settings > Agents > Approvals and Execution actually shows.
  2. Audit the shell startup files for changes you did not make, using the commands below. Do this even after patching, because the persistence variant leaves a file behind.
  3. Cut the allowlist down to read-only commands you would run blind. Allowlisted calls run outside the sandbox, so every entry is an unsandboxed lane.
  4. Turn the sandbox on and close the network with a project-level sandbox.json, then open only the domains the project needs. Commit the file so it applies to the whole team.
  5. Ban Run Everything and insecure_none on any machine that holds production credentials or deploy keys.
  6. Treat anything the agent reads as untrusted input: READMEs, issues, dependency docs. NVD names indirect prompt injection as the delivery route.
# Step 2: audit startup files. A write here survives a Cursor update.
# Newest first: anything modified recently that you did not touch is suspect.
ls -lt ~/.zshenv ~/.zprofile ~/.zshrc ~/.zlogin \
       ~/.bash_profile ~/.bashrc ~/.profile 2>/dev/null

# Variables Pillar names as hijackable. None belong in a startup file
# unless you put them there on purpose.
grep -nE 'PAGER|LD_PRELOAD|GIT_ASKPASS' \
  ~/.zshenv ~/.zprofile ~/.zshrc ~/.zlogin \
  ~/.bash_profile ~/.bashrc ~/.profile 2>/dev/null

# Hygiene only: GIT_PAGER and core.pager outrank PAGER, but anyone able to set
# variables can set GIT_PAGER too. This does not close the vulnerability class.
git config --global core.pager less

The sandbox.json below uses only fields from Cursor's reference page. readBoundary requires Cursor 3.23 or later per that page, so drop it on older builds. The permissions.json steers Auto-review in plain English; Cursor says Auto-review is not a security boundary, so treat it as a nudge on top of the sandbox rather than a replacement.

// <project>/.cursor/sandbox.json  (commit it; per-repo wins over ~/.cursor/sandbox.json)
{
  "type": "workspace_readwrite",        // never "insecure_none": that disables the sandbox
  "networkPolicy": {
    "default": "deny",                  // the documented default, stated on purpose
    "allow": ["registry.npmjs.org"]     // exact domains; add one per real need
  },
  "readBoundary": "workspace",          // Cursor 3.23 or later: ask before reading outside the workspace
  "additionalReadPaths": [],
  "disableTmpWrite": false
}

// <project>/.cursor/permissions.json  (plain-English steering for Auto-review)
{
  "autoRun": {
    "allow_instructions": [],
    "block_instructions": [
      "Any command that sets, changes or unsets environment variables such as PAGER, LD_PRELOAD or GIT_ASKPASS should go through approval first."
    ]
  }
}

git config --global core.pager less is hygiene, not a fix. Git's order puts GIT_PAGER and core.pager ahead of PAGER, but an attacker who can set variables can set GIT_PAGER too. Do not mistake pinning a pager for closing the class.

The rule I take from this: an approval attaches to a command, but a command's behaviour depends on state that earlier commands changed. Any control that approves strings without controlling the environment they run in will eventually approve the wrong thing. Patch to 2.3, audit your startup files, keep the allowlist tiny, and put the real boundary in a sandbox that you do not casually opt out of.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

What is CVE-2026-22708 in Cursor?

CVE-2026-22708 is a vulnerability in Cursor versions before 2.3. With Auto-Run Mode and the allowlist enabled, some shell built-ins ran without allowlist checks or approval, letting a prompt-injected agent change environment variables that trusted commands rely on. It is fixed in Cursor 2.3.

Why do the CVSS scores for CVE-2026-22708 differ?

NVD scores it 9.8 Critical under CVSS 3.1, while GitHub, acting as the CNA, scores it 7.2 High under CVSS 4.0 with high attack complexity. They are different scoring systems applied by different parties. Read the precondition, a non-default allowlist setup plus a prompt injection, before relying on either number.

How can a git branch command lead to code execution?

Git picks its pager from GIT_PAGER, then core.pager, then PAGER, and its manual says the value is meant to be interpreted by the shell. Pillar Security showed that a built-in could set PAGER silently, so a later allowlisted git command ran attacker-controlled code. The approval covered the git command, not the poisoned environment.

Are command allowlists safe for AI coding agents?

They are a convenience, not a security boundary. An allowlist matches command names, while attacks can live in the environment, arguments or parser gaps. Cursor's docs describe the Allowlist mode as deterministic behaviour for a small set of trusted repeat actions, and note allowlisted calls run outside the sandbox.

What should I do after patching Cursor to 2.3?

Audit shell startup files such as ~/.zshrc for changes you did not make, since a file write persists after an update. Shrink the allowlist to read-only commands, enable the sandbox with a project sandbox.json that denies network by default, and avoid Run Everything on machines holding production credentials.