T20 — 6-layer hierarchical permission evaluation + path normalization
Tổng quan Overview
Hầu hết agent harness implement permission check như một điều kiện đơn hoặc danh sách flat rules. OpenHarness chọn kiến trúc phân tầng: 6 layers đánh giá tuần tự, mỗi layer chỉ chạy nếu layer trước không đưa ra quyết định. Thứ tự không phải tùy tiện — nó phản ánh mức độ ưu tiên từ "tuyệt đối" (sensitive paths) đến "mặc định theo cấu hình" (mode).
_normalize_path_variants() sinh cả "/secret" lẫn
"/secret/" — pattern rules được check với cả hai variant.
Phân tích code: permissions/checker.py:75-169 Anatomy
PermissionChecker.check() — 6-layer pipeline
permissions/checker.py:75-130 — main check() method
{`
class PermissionChecker:
def check(
self,
tool_name: str,
path: str | None,
command: str | None,
context: PermissionContext,
) -> CheckResult:
# Layer 1: sensitive path (unconditional)
if path and is_sensitive_path(path):
return CheckResult.BLOCK("Sensitive path")
# Layer 2: deny tool list
if tool_name in self._deny_tools:
return CheckResult.BLOCK(f"Tool {tool_name!r} is denied")
# Layer 3: allow tool list (early exit — skip remaining layers)
if tool_name in self._allow_tools:
return CheckResult.ALLOW()
# Layer 4: path rules
if path:
result = self._check_path_rules(path)
if result is not None:
return result
# Layer 5: command deny patterns
if command:
result = self._check_command_deny(command)
if result is not None:
return result
# Layer 6: permission mode (fallback)
return self._check_mode(tool_name, context.mode)
`}
Mỗi layer trả về CheckResult (BLOCK hoặc ALLOW) để short-circuit,
hoặc None để fall through đến layer tiếp theo. Layer 3 (allow tool list)
là điểm đặc biệt: nếu tool nằm trong allow list, các layers 4-5 bị bỏ qua hoàn toàn
— ngay cả khi path rules hay command patterns có thể block nó.
Path normalization — chặn trailing-slash bypass
permissions/checker.py:131-155 — _normalize_path_variants() và _check_path_rules()
{`
def _normalize_path_variants(path: str) -> list[str]:
"""
Generate normalized variants to prevent bypass via trailing slash.
'/secret/' vs '/secret' both checked.
"""
stripped = path.rstrip("/")
return [stripped, stripped + "/"]
def _check_path_rules(self, path: str) -> CheckResult | None:
variants = _normalize_path_variants(path)
for rule in self._path_rules:
for variant in variants:
if fnmatch.fnmatch(variant, rule.pattern):
if rule.action == "deny":
return CheckResult.BLOCK(f"Path denied by rule: {rule.pattern}")
if rule.action == "allow":
return CheckResult.ALLOW()
return None # no rule matched, continue to next layer
`}
Trailing-slash bypass là kỹ thuật cũ nhưng vẫn hiệu quả: nếu rule deny
"/secret" nhưng agent request "/secret/", pattern matching
đơn giản sẽ miss. _normalize_path_variants() sinh ra cả hai —
rule chỉ cần match một trong hai là đủ để block.
Adversa CC-643 — pipeline bypass và mitigation
permissions/checker.py — CC-643 fix: check_full_pipeline()
{`
# Adversa CC-643: 50-subcommand bypass
# Attack: instead of "rm -rf /important", agent uses:
# "echo x | cmd1 | cmd2 | ... | cmd49 | rm -rf /important"
# Each subcommand individually matched "allowed" patterns
# The full pipeline was never checked as a unit
# OpenHarness mitigation: check ALL subcommands in pipeline
import shlex
def check_full_pipeline(command: str) -> bool:
"""Check each segment of a piped command."""
segments = command.split("|")
for seg in segments:
tokens = shlex.split(seg.strip(), posix=True)
if tokens and _is_dangerous_command(tokens[0]):
return False # block entire pipeline
return True
# Khi _check_command_deny() nhận command, nó gọi check_full_pipeline()
# thay vì chỉ check whole string một lần.
# "echo x | cat /etc/passwd" → segment 2 fail → toàn pipeline bị block
`}Tương tác với các kỹ thuật khác Interaction
Failure modes Failures
Failure 1: Adversa CC-643 nếu pipeline check không đầy đủ
CC-643 variant: nested subshell bypass
{`
# check_full_pipeline() split theo "|" — nhưng Bash có nhiều cách
# compose commands không dùng pipe:
# Semicolon: "safe_cmd; rm -rf /important"
# split("|") → ["safe_cmd; rm -rf /important"] — 1 segment, pass check
# shlex sees: ["safe_cmd;", "rm", "-rf", "/important"]
# _is_dangerous_command("safe_cmd;") → False
# Subshell: "safe_cmd $(rm -rf /important)"
# Nested command substitution không bị detect bởi simple split
# Fix: parse với shlex.split() + check từng token,
# hoặc dùng AST bash parser (bashlex library)
# Đây là lý do tại sao "deny rules block tool invocations, not Bash subprocesses"
`}Failure 2: Allow tool list quá rộng — bypass layers 4-5
Kịch bản: bash_tool trong allow_tools → command không được inspect
{`
# Cấu hình: allow_tools = {"bash_tool", "read_file", "glob_tool"}
# Agent call: bash_tool(command="rm -rf /project/src")
# Layer 1: path=None → skip T19
# Layer 2: "bash_tool" not in deny_tools → continue
# Layer 3: "bash_tool" IN allow_tools → ALLOW (short-circuit)
# Layers 4-5: SKIPPED — never reached
# Command "rm -rf /project/src" không bao giờ được check
# Nguyên tắc: NEVER put bash_tool in allow_tools
# Thay vào đó: để bash_tool fall through đến layer 5 (command deny)
# và define DANGEROUS_CMDS patterns đủ rộng
`}Failure 3: Path rules conflict — child rule override parent deny
Rule ordering: deny parent nhưng allow child — child match trước
{`
# Rules (theo thứ tự iteration):
# rule 1: pattern="/project/src/*", action="allow"
# rule 2: pattern="/project/*", action="deny"
# Request: path="/project/src/secret.ts"
# _check_path_rules() iterate rules theo thứ tự:
# - rule 1: fnmatch("/project/src/secret.ts", "/project/src/*") → True
# → action="allow" → ALLOW (return ngay)
# - rule 2: never reached
# Ý định: deny toàn bộ /project/ trừ src/
# Kết quả: correct IF rule order is [deny /project/*, allow /project/src/*]
# Nhưng nếu thứ tự ngược lại → allow rule match trước → bypass deny
# Fix: document rõ "first-match wins" và sort rules: specific trước, broad sau
# Hoặc implement priority field trong rule definition
`}So sánh với các harness khác Compare
| Harness | Số layers | Path normalization | Pipeline check | Deny bypass qua Bash |
|---|---|---|---|---|
| OpenHarness | 6 layers (ordered, short-circuit) | Có — trailing slash variants | Có — CC-643 fix (split("|")) | Có (documented: allow list quá rộng) |
| Claude Code | Hierarchy tương tự | Partial | Partial | Có (documented) |
| Aider | 1 layer (--yes flag) | Không | Không | N/A |
| LangGraph | Node-level interrupt | Không | Không | N/A |
OpenHarness và Claude Code đều có hierarchy nhiều layers, nhưng OpenHarness document
rõ thứ tự và fix Adversa CC-643. Aider dùng --yes flag đơn giản —
không có hierarchy. LangGraph interrupt ở node level trong graph — không phải
tool-call level.
Implementation recipe Recipe
Minimal 6-layer permission checker — đủ dùng cho agent harness mới, bao gồm path normalization và pipeline check:
permission_checker.py — minimal 6-layer implementation
{`
import fnmatch
DANGEROUS_CMDS = {"rm", "dd", "mkfs", "curl", "wget", "nc", "python", "python3"}
def check_permission(
tool: str, path: str | None, cmd: str | None,
deny_tools: set, allow_tools: set, path_rules: list, mode: str
) -> str: # "allow" | "block" | "ask"
# L1: sensitive paths (từ T19)
if path and is_sensitive(path):
return "block"
# L2: explicit deny
if tool in deny_tools:
return "block"
# L3: explicit allow (short-circuit — skip L4-L5)
if tool in allow_tools:
return "allow"
# L4: path rules (first-match wins, specific rules first)
if path:
variants = [path.rstrip("/"), path.rstrip("/") + "/"]
for rule in path_rules:
for v in variants:
if fnmatch.fnmatch(v, rule["pattern"]):
return rule["action"] # "allow" or "block"
# L5: command deny (check full pipeline)
if cmd:
for seg in cmd.split("|"):
import shlex
try:
tokens = shlex.split(seg.strip(), posix=True)
except ValueError:
return "block" # unparseable → block
if tokens and tokens[0] in DANGEROUS_CMDS:
return "block"
# L6: mode fallback (từ T18)
if mode == "full_auto":
return "allow"
if mode == "plan":
# block mutations, allow reads
WRITE_OPS = {"bash_tool", "write_file", "delete_file"}
return "block" if tool in WRITE_OPS else "allow"
return "ask" # DEFAULT: ask user
# Lưu ý triển khai:
# - L3 allow_tools KHÔNG nên bao gồm bash_tool (bypass L4-L5)
# - path_rules nên sort: specific (dài hơn) trước broad (ngắn hơn)
# - L5 split("|") chưa handle semicolon hay subshell — cần bashlex cho full coverage
`}Tham khảo Refs
- Anthropic — Claude Code Security model · Permission layering, tool allow/deny, path-based rules trong Claude Code upstream
- Adversa AI — We Hacked Claude (CC-643) · Kỹ thuật 50-subcommand pipeline bypass và disclosure timeline
- OWASP — Path Traversal · Trailing-slash bypass và các path normalization pitfall phổ biến
- Python docs — fnmatch · Giới hạn của glob matching: ** behavior, trailing slash, case sensitivity
- HumanLayer — Harness Engineering for Coding Agents · Multi-layer permission design: rationale và trade-offs
- bashlex — Python Bash parser · Full Bash AST parsing để detect subshell và semicolon bypass trong L5