Skip to content

fix(security): fail closed on plan-approval declines; add frontend security headers - #53

Open
nithiink wants to merge 1 commit into
mainfrom
security-fixes-2026-07-21
Open

fix(security): fail closed on plan-approval declines; add frontend security headers#53
nithiink wants to merge 1 commit into
mainfrom
security-fixes-2026-07-21

Conversation

@nithiink

Copy link
Copy Markdown
Owner

Fixes the auto-fixable findings from the 2026-07-21 daily security review. Refs #52.

Fixes (mapped to findings)

M1 — backend/claude_runner.py (SDKClaudeRunner._map_decision, ExitPlanMode branch)

The branch approved the plan whenever the answer contained an intent word (auto/manual/proceed/approve), even after decide_permission had already resolved it to deny. So a decline such as "do not proceed" (which contains "proceed") was inverted into PermissionResultAllow(), defeating the module's documented fail-closed contract.
Fix: short-circuit on an explicit deny before the intent-substring test.

M2 — backend/tmux_runner.py (_classify_plan_choice)

The manual/auto intent substrings ("manual", "approve edit", "each edit", "review edit", "auto") were tested before the decline branch, and that decline branch only caught negations at the start of the phrase. "do not approve edits" matched "approve edit"manual, and a phrase like "please don't switch to auto" matched "auto" → escalated to auto-approve-everything mode.
Fix: evaluate an explicit decline (decide_permission(c) == "deny") first.

L1 — frontend/next.config.mjs

Added a headers() block applying X-Frame-Options: DENY, CSP frame-ancestors 'none', X-Content-Type-Options: nosniff, and Referrer-Policy: no-referrer to all routes — clickjacking protection + defense-in-depth for the command-proxying UI (which binds 0.0.0.0 in network mode).

Verification

decide_permission was checked in isolation to confirm the fixes preserve the happy paths:

  • Declines that contain intent words → deny (now correctly short-circuited): "do not proceed", "don't approve", "do not approve edits", "please don't switch to auto".
  • Genuine approvals → still approved: "proceed", "manually approve edits", "auto" (→ None, handled by the existing intent path), "yes", "approve".

No existing test references _map_decision, _classify_plan_choice, or SAFE_TOOLS, so behavior of the covered paths is unchanged. (The full pytest suite couldn't run in the review sandbox — claude_agent_sdk isn't installed there — so please let CI exercise it.)

Left for manual attention (not in this PR)

🤖 Generated with Claude Code

https://claude.ai/code/session_01BJwDxJqz2ckLKHUTraLqLf


Generated by Claude Code

…curity headers

Addresses findings from the 2026-07-21 daily security review (#52).

M1 (claude_runner.py): the SDK ExitPlanMode branch approved a plan whenever
the answer contained an intent word ("auto"/"manual"/"proceed"/"approve"),
even after decide_permission had resolved it to "deny". A decline such as
"do not proceed" (contains "proceed") was inverted into approval. Short-circuit
on an explicit deny before the intent-substring test. Genuine approvals
("proceed", "manually approve edits", "auto") still resolve to allow.

M2 (tmux_runner.py): _classify_plan_choice checked the manual/auto intent
substrings before the decline branch, and the decline branch only caught
negations at the start of the phrase. "do not approve edits" matched
"approve edit" -> manual, and "...auto..." could escalate to auto-approve
mode. Evaluate an explicit decline (decide_permission == "deny") first.

L1 (next.config.mjs): add X-Frame-Options/CSP frame-ancestors, nosniff, and
Referrer-Policy headers to the command-proxying frontend (clickjacking +
defense-in-depth).

M3 (WebFetch auto-approved) is a product/UX tradeoff and is left for a
maintainer decision, not auto-fixed here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJwDxJqz2ckLKHUTraLqLf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants