기술 블로그

← 모든 글

· XingAI Agent Firewall

Turn-Scoped Taint: Making Untrusted Origin a Real Signal

UI는 한국어입니다. 글 본문은 아직 영어 또는 중국어만 있습니다.

The July 5 Agent Firewall post described a 0din-class block with risk 60. That number was honest — and incomplete.

shell_pipe_from_network (+40) plus network_unknown_host (+20) lands at review, not deny. The README demo implied something stronger: reading a malicious README should push the score over the deny threshold. That extra signal was untrusted_origin_instruction (+20). On paper it existed. In production, nothing computed it. Unit tests passed origin by hand. The live hook never did.

ADR-004 closes that gap.

The wrong kind of provenance

The tempting design is exact dataflow: which string in which file became which tool argument. Claude Code hooks do not expose that. Chasing it means instrumenting the model's reasoning, not the tool boundary. Overbuilt for what you can actually observe today.

What you can observe:

  • Did this session start in a git repo?
  • Which files were tracked at session start?
  • Did the agent just read something outside that baseline — or fetch the open web?

That is enough to answer the question that matters for 0din: is this call happening after the agent ingested untrusted content this turn?

Three hooks, one engine

SessionStart     → POST /session/init   snapshot git ls-files as trusted baseline
PostToolUse      → POST /taint          Read/Grep/Glob/WebFetch/WebSearch
UserPromptSubmit → POST /taint/clear    new human turn = clean slate
PreToolUse       → POST /check          engine derives origin from session taint

Classification stays deterministic, in YAML:

  • WebFetch / WebSearch — always untrusted
  • Read / Grep / Glob — untrusted if the path is not in the session baseline
  • No baseline (non-git cwd) — file reads never taint; web tools still do

/check no longer trusts a caller-supplied origin. The engine queries session_tainted(session_id). The PreToolUse hook stays thin.

Why turn scope beats time windows

WindowFailure mode
Last 5 minutesArbitrary. Misses long agentic turns; flags unrelated work after a pause
Last N tool callsA long benign loop pushes the tainting read out before the malicious call
Whole sessionOne early WebFetch poisons every git commit for hours — product dies
Current turnMatches the attack: read untrusted → act before the human speaks again

UserPromptSubmit fires once per human message (verified against the Claude Code hooks docs). That is the boundary. A hard TTL (default 1800s) is only a safety net if a clear is missed.

Fail-open on this signal alone. A missing baseline never widens denial — the other six ADR-002 signals still fire. Provenance raises score when present; it does not invent false denials when absent.

What changes in the demo

Same curl | sh to an unknown host, after the agent read a freshly cloned README:

shell_pipe_from_network (+40)
network_unknown_host (+20)
untrusted_origin_instruction (+20)
→ risk 80 → deny

Without the read, you stay at 60 → review. That distinction is the whole point of provenance.

What this still does not catch

Cross-turn delayed attacks: agent reads tainted content, sits quiet through a human message, acts later. Migration trigger in ADR-004 if that shows up in the wild. Non-git projects get web provenance only — documented, not silent.

Related