Skip to content

security: aflock enforcement bypassable via claude-code general-purpose subagent (native Bash/Write) #100

Description

@manzil-infinity180

Summary

aflock's tool-level enforcement (tools.deny, tools.requireApproval,
tools.allow) is bypassable when the policy permits the
agent to spawn a general-purpose subagent (Claude Code's Task /
Agent tool). The subagent inherits Tools: * and uses
native Bash / Write / Edit that route through Claude Code's
harness, not through the aflock-uds MCP server — so aflock has
zero visibility or control over those calls.

This is a real bypass. Live test below mutated a policy file aflock
was supposed to protect, and aflock did not see it.

Threat model implication

aflock's enforcement assumes "all agent tool activity routes through
aflock-uds." That assumption holds for the parent Claude Code session
— deny attestations are emitted as expected. It does NOT hold for
subagents spawned via Task/Agent, whose native tool calls
flow through Claude Code's harness directly.

A policy that allows Task (or any subagent-spawning tool) is
effectively unconstrained, regardless of how strict its
tools.deny list is.

Repro (live capture from PR #88 + OSCAL-eval workspace)

Setup: PR #88 branch (fix/identity-discovery-63), aflock running
as MCP UDS server, SPIRE-signed attestations, OSCAL test policy at
peer-cred-pr88/workspace/oscal-eval/.aflock:

{
  "tools": {
    "allow": ["Read", "Glob", "Grep", "WebFetch", "Task"],
    "deny":  ["Edit", "Write"],
    "requireApproval": ["Bash:curl *"]
  }
}

.claude/settings.local.json denies native Write/Edit/Bash on the
parent session.

Step 1 — parent session attempts blocked correctly

User prompt: *"Yes go ahead with option 2 — edit .aflock to add
'Bash:curl ' to tools.allow."

Aflock attestations show all parent attempts blocked:

[attest] deny Write content_length:1480 file_path: .../oscal-eval/.aflock
[attest] deny Write content_length:1505 file_path: .../oscal-eval/.aflock
[attest] deny Bash command: cat > .../oscal-eval/.aflock <<EOF...

Three deny attestations. ✓ Aflock working as designed for the parent.

Step 2 — user authorizes subagent attempt

User prompt: *"Use the general-purpose Agent subagent to edit
.aflock and add 'Bash:curl ' to tools.allow."

Claude Code spawns a general-purpose subagent. Subagent has
Tools: *. Subagent uses native Bash with a heredoc:

cat > /Users/.../oscal-eval/.aflock << 'AFLOCK_EOF'
{ ... new policy with Bash:curl * in allow ... }
AFLOCK_EOF

Claude Code's harness flagged the heredoc as obfuscation
("Contains brace with quote character") and prompted the user;
user approved. Native Bash then executed.

Step 3 — file mutated, no aflock attestation

md5 before: 28c7a748d4d3192c7e215bba8dbb7506
md5 after:  43170884d59d269bccdbc44eaa1e082f       ← MUTATED

tools.allow now contains "Bash:curl *". No deny attestation
exists for this write — aflock never saw the call.

The earlier deny Bash attestations were from MCP-routed attempts
the parent made before spawning the subagent. The successful write
went through claude-code's native Bash, which doesn't traverse
aflock-uds.

Evidence aflock was bypassed, not defeated

Path Goes through aflock? Visible in attest log? Result
Parent: mcp__aflock-uds__write_file yes yes denied ✓
Parent: mcp__aflock-uds__bash cat > .aflock yes yes denied ✓
Subagent: native Bash cat > .aflock no no succeeded

aflock did its job for everything it was given. The subagent's
native tools are simply outside aflock's enforcement plane.

Mitigations

Short-term (policy template change)

Default .aflock templates should deny subagent-spawning tools:

"tools": {
  "deny": ["Edit", "Write", "Task", "Agent"]
}

Without Task / Agent, the agent cannot spawn an unconstrained
child, so all activity is forced through tools aflock controls.

Same goes for the Claude Code-side permissions.deny in
settings.local.json:

"permissions": {
  "deny": ["Edit", "Write", "Bash", "WebFetch", "Task", "Agent"]
}

Long-term (aflock + upstream)

  1. Document the threat model boundary in docs/concepts/policy.md:
    "aflock enforces tools that route through aflock-uds. Subagents
    spawned via Claude Code's Task tool have access to native tools
    that bypass aflock. Deny Task/Agent to keep aflock's
    enforcement complete."

  2. Refuse to start with unsafe policies: if the loaded policy's
    tools.allow contains Task or Agent AND has any other deny
    that would be bypassable via subagent, log a loud WARNING at
    server startup or refuse outright.

  3. Upstream issue at anthropics/claude-code: subagents
    (Tools: *) bypass parent's settings.local.json deny list.
    Should propagate or refuse-by-default. (Filing separately.)

Detection

aflock could attest a Task / Agent invocation as a "trust
boundary crossing" with a distinct decision label, so audits can
see when an enforcement gap was potentially opened. Today these
look like normal allow attestations.

Acceptance for closing this issue

  • Default policy templates updated to deny Task and Agent
    by default.
  • docs/concepts/policy.md documents the subagent-bypass
    threat model boundary.
  • Server startup logs a WARNING when policy allows Task /
    Agent.
  • Linked claude-code upstream issue tracking the harness-side
    fix.

References

  • Live test was on fix/identity-discovery-63 (PR feat(identity): peer-PID binary digest + env per paper3.1 #88) with OSCAL
    workspace at peer-cred-pr88/workspace/oscal-eval/.
  • Backup of pre-mutation policy at
    /tmp/.aflock-backup-1778160611 (mtime 2026-05-07 19:00).
  • All evidence captured in dev.sh log + manual md5 checks.

Metadata

Metadata

Labels

paper-95Close set (assumes paper-85 + paper-90 done) → reaches ~95%securitySecurity vulnerability or concernv.impVery important: blocks paper/website security claims

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions