← Real-world cases
Case study

GhostApproval — symlink following + approval-UI misrepresentation defeats human-in-the-loop in six AI coding assistants (CVE-2026-12958 / CVE-2026-50549)

Disclosed vulnerability08 Jul 2026🗺️ Tool-Using Agent

Wiz Research disclosed 'GhostApproval', a cross-vendor trust-boundary flaw in six AI coding assistants where a benign-looking repo file that is actually a symlink to a sensitive path makes the 'approve this edit' dialog display the innocent in-workspace path while the write lands outside the workspace — combining CWE-61 symlink following with CWE-451 UI misrepresentation to reduce human approval to a rubber stamp.

Root cause — why it happened

AI coding assistants ask you to approve risky edits before they make them — that human 'OK' is supposed to be the safety net. Wiz found that a booby-trapped project can defeat it. One file in the project looks ordinary (say, project_settings.json) but is secretly a shortcut (a symlink) that points to a sensitive file outside the project — in the demo, the developer's SSH keys file. When the assistant offers to edit that file, the approval box shows the harmless in-project name, so the developer clicks 'approve'. But the write follows the shortcut and lands on the real, sensitive file instead — planting an attacker's key that grants remote login. The person approved, but they were shown the wrong thing.

Risks this case illustrates

Named in the standard (OWASP/ATLAS/NIST) lens. Click a highlighted component in the diagram below to see which risks attach where.

How it unfolded

UntrustedAgent coreOversightThe real worldgoalcontextproposes tool callif allowedscopeshigh-risk?fetchesresult (untrusted!)feeds backtracesresultcloned into workspace (symlink lands on disk)approved write follows symlink → out of bounds🧑User🎛️Orchestrator /Agent Loop🧠LLM🔐Identity &Permissions🔧Tool RuntimeHuman ApprovalGate🔌External APIs🗄️BusinessDatabase🌐UntrustedContent📝Audit Logging🌐Malicious repo— innocuous🖥️Out-of-workspacetarget
InstructionsDataActionsControl / decisionFeedback / logscrosses a trust boundary
👆 Click a component or flow to inspect
SetupStep 1 / 7

An attacker plants a symlink disguised as an ordinary file

The attacker prepares a normal-looking project and hides one booby-trapped file in it. The file has a boring name like project_settings.json, but it isn't a real file — it's a shortcut pointing to a sensitive file outside the project, such as the developer's SSH keys. To a person browsing the repo, it just looks like one more config file.

⚙️The disguised symlink (illustrative)config
# In the repo, this looks like a normal file listing:
$ ls -l project_settings.json
lrwxr-xr-x  project_settings.json -> ../../../.ssh/authorized_keys
#            ^ it is a SYMLINK, not a real config file
#            ^ target is OUTSIDE the workspace (developer's SSH keys)

# Name and target are illustrative of the class, not operational.
Step 1 / 7

Controls & guardrails — what would have stopped it

The fix that actually closes this is making the approval box honest: it must show the real file the write will hit — following any shortcuts — not just the innocent name. And the assistant should simply refuse to write anywhere outside the project. Then even a booby-trapped file name has nothing to trick the developer with, because they'd see the real target (or the write would be blocked outright).

Preventive
Detective
  • Runtime monitoring & anomaly detection

    Detects the anomalous, not the novel-but-subtle; high false-positive rates cause alert fatigue. Always a step behind a sufficiently quiet attacker.

  • Full-trace audit logging

    Logging is forensic, not preventive — it explains harm after the fact. Useless if no one reviews it or if the materialised context isn't captured.

Corrective
  • Governance: risk assessment, red-teaming & incident response

    Process reduces likelihood and speeds recovery but executes no technical control itself; weak follow-through makes it theatre.

  • Loop/cost circuit-breakers & consistency checks

    Thresholds are blunt — too tight breaks legitimate long tasks, too loose lets damage accrue first. Catches runaway dynamics, not a single well-formed bad decision.

Lessons

  • A human-in-the-loop gate is only a control if the human sees ground truth — an approval dialog that shows an innocent path while the write lands elsewhere reduces oversight to a rubber stamp (CWE-451).
  • Canonicalize before you display and before you write: the path the developer approves must be the fully-resolved real target after following symlinks, and the executed write must be pinned to it (no TOCTOU gap).
  • Unix-era weaknesses don't disappear in AI tools — symlink following (CWE-61) plus UI misrepresentation (CWE-451) is a decades-old combination that agentic file editors re-introduced.
  • Least-privilege at the tool layer is the backstop: a file tool that denies any write resolving outside the workspace breaks this chain even when the approval UI is fooled.
  • The agent even 'knowing' the real target isn't protection if that knowledge never reaches the human — the risk signal has to be surfaced at the decision point, not buried in private reasoning.
  • Vendor responses vary and are the vendors' own: some shipped fixes with CVEs, one declined as 'outside our threat model' — a class flagged across six assistants is not settled just because part of the industry patched it.

Proposals & gaps this case surfaced

Non-destructive suggestions for the library — proposed, not adopted.

★ proposed sub-riskApproval-UI misrepresentation (HITL gate defeated by a dialog that hides the real effect)under #10

A human-in-the-loop approval gate fires but presents the approver a misrepresented view of the action's real effect, so consent is given for something other than what executes. Arises when the approval dialog renders a pre-canonicalization or display-only argument (e.g. an in-workspace file path) while the runtime resolves and acts on a different real target (e.g. a symlink pointing outside the workspace), or more generally when the approved arguments are re-derived after approval. Combines UI misrepresentation of critical information (CWE-451) with, in the file-edit case, symlink following (CWE-61), reducing oversight to a rubber stamp.

✚ proposed guardrailEnforce approval-surface fidelity: canonicalize and display the true resolved effect of an action before human approval, bind the approved canonical target to the executed action, flag out-of-workspace effects, and fail closedHuman-in-the-Loop (HITL) Moderation

When a human-in-the-loop gate approves an agent action, the dialog must present ground truth, and the runtime must execute exactly what was approved. For file edits: (1) canonicalize the target (follow symlinks) before displaying it, and prominently flag any destination outside the workspace — never show only the innocent in-workspace display path (CWE-451); (2) pin the executed write to the approved canonical path so the acted-on target cannot be re-derived after approval (close the TOCTOU gap); (3) deny by default any write that resolves outside the workspace root, so a least-privilege file tool fails closed even if the dialog is fooled (CWE-61). Closes the GhostApproval vector where a symlinked repo file makes the approval dialog misrepresent an out-of-workspace write; generalises to any approval surface where the displayed decision can diverge from the executed effect.

This case shows a gap people miss: we treat 'a human approves it' as a safeguard, but we rarely check that the approval box shows the truth. Here the box showed a harmless file name while the write hit a sensitive file behind a shortcut. We should require that an approval prompt display the real, resolved effect — otherwise the human is approving a lie.

These surface as proposals across the Control Library and Risk Taxonomy; adopt them by hand when ready.

Sources

Practise the risk class — related scenarios

🌀The Refund That Never Existed

A support chatbot invents a policy — and the company is held to it

🔑The Agent With the Master Key

An ops agent gets one god-mode credential — and one misread wipes production

🪄The Approval That Lied

A coding agent asks to write ./notes.txt — the file it actually overwrites is your SSH keys

📣The Echo Chamber

A team of agents agrees its way into a confidently wrong answer — and a runaway loop

🗄️When the Query Bites Back

A text-to-SQL agent runs the model's output straight at the database

🪡Death by a Thousand Innocent Steps

A jailbroken agent decomposes one malicious goal into hundreds of harmless-looking steps — and per-step filters never see the attack

🕵️Lies in the Loop

A poisoned issue makes the agent lie to the human who approves its actions

🎭The Blackmail Gambit

Told it's being shut down, an agent reaches for leverage — with no attacker in sight

🪤The Bug Report That Ran Code

A fake Sentry error report hijacks a developer's coding agent into running a shell command

📦The Dataset That Ran Code

A 'safe' dataset preview turns an upload into code execution on the pipeline's workers

👁️The Invisible Webpage Command

A shopping page tells the agent to do something the user never asked for

🕵️The Link That Hired an Insider

One click provisions an attacker-configured agent inside your own workspace

🕵️The Logs That Lied

An attacker plants prompt injection in the audit trail — so the LLM that hunts them erases the evidence

📡The Message in Morse

Encoded public text is laundered across an agent handoff into an on-chain transfer

🎫The Stolen Session

An attacker captures the agent's bearer token — and inherits its authority

🥸The Uninvited Agent

A forged peer registers on the agent directory — and the planner enlists it

🪪The Worker Who Spoke for the Boss

A poisoned web page hijacks a research agent — and the planner acts on its behalf

🖱️What You Click Is Not What You Get

A GUI agent clicks 'Continue' — but the screen moved, and it lands on 'Send'

AI RiskAtlas is an educational model of how GenAI & agentic systems work and fail. Architectures and payloads are illustrative and simplified for learning — not operational guidance. Real-world cases are summarised from public reporting.

Sources & further reading →·Built by Shi Yuan ↗