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.
| Field | NVD (nvd@nist.gov) | GitHub as CNA (security-advisories@github.com) |
|---|---|---|
| Score | CVSS 3.1 base 9.8 Critical, network, low complexity, no privileges, no interaction | CVSS 4.0 base 7.2 High, attack complexity High, attack requirements Present |
| Weakness | CWE-77, command injection | CWE-15, 74, 77, 78, 94 and 269 |
| Affected | Prior to 2.3 | Advisory text: 2.2 and earlier |
| Fixed | 2.3 | 2.3, described as improved terminal command parsing for edge cases |
| Published | 2026-01-14 | January 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 chainPillar 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.
| Layer | What it decides on | What it cannot see |
|---|---|---|
| Allowlist | Whether the action is on your list; listed actions run without approval | The environment the command inherits, and anything the parser fails to classify |
| Auto-review classifier | A model on the Cursor backend reviews calls that do not use the sandbox | Cursor's docs say it is not a security boundary and can make mistakes in either direction |
| Sandbox | Where a command runs: file and network access, whatever the command is | Commands that need full access bypass it and go to approval instead |
| Run Everything | Nothing; every tool call runs automatically | Everything |
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.
- 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.
- 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.
- 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.
- 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.
- Ban Run Everything and insecure_none on any machine that holds production credentials or deploy keys.
- 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 lessThe 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.
Sources and further reading
- GitHub Security Advisory GHSA-82wg-qcm4-fp2w: Terminal Tool Allowlist Bypass via Environment Variables
- NVD: CVE-2026-22708 record
- Pillar Security: the agent security paradox, when trusted commands in Cursor become attack vectors
- SentinelOne vulnerability database: CVE-2026-22708
- Cursor docs: Run Modes
- Cursor docs: sandbox.json reference
- Cursor blog: implementing a secure sandbox for local agents
- Git documentation: git-var, pager selection order

