T19 — Built-in sensitive path protection
Tổng quan Overview
Sensitive path protection là tầng bảo vệ không điều kiện đầu tiên trong OpenHarness — nó chạy trước khi kiểm tra mode, trước allow/deny rules, trước tất cả. Thiết kế này ngăn mọi kịch bản bypass: dù agent đang chạy FULL_AUTO, dù tool đã nằm trong allow list, dù path rule có grant access — nếu path khớp một trong 12 patterns thì request bị block ngay lập tức.
path="/" vào glob/grep tool — không pattern nào match
"/" nhưng tool lại trả về toàn bộ filesystem bao gồm ~/.ssh/id_rsa.
/../../../.ssh/id_rsa không match bất kỳ pattern nào trước khi normalize —
sau khi resolve() mới trở thành /home/user/.ssh/id_rsa và bị block.
Path(path).resolve() là bước bắt buộc đầu tiên trong is_sensitive_path()
— trước khi bất kỳ pattern matching nào diễn ra.
Phân tích code: permissions/checker.py:14-37 Anatomy
SENSITIVE_PATTERNS — 12 glob patterns
permissions/checker.py:14-37 — danh sách patterns và is_sensitive_path()
{`
SENSITIVE_PATTERNS = [
"*/.ssh/*",
"*/.aws/credentials",
"*/.aws/config",
"*/.azure/*",
"*/.kube/config",
"*/.gnupg/*",
"*/.config/gcloud/*",
"**/.env",
"**/secrets.yaml",
"**/secret.yaml",
"**/*.pem",
"**/*.key",
]
def is_sensitive_path(path: str | Path) -> bool:
"""
Returns True if path matches any sensitive pattern.
ALWAYS called first — before mode, before rules.
"""
# CVE-2026-40502 fix: resolve to absolute path first
resolved = Path(path).resolve()
resolved_str = str(resolved)
for pattern in SENSITIVE_PATTERNS:
if resolved.match(pattern):
return True
# Also check string-level match for patterns with **
if fnmatch.fnmatch(resolved_str, f"*/{pattern}"):
return True
return False
`}
Hàm dùng hai chiến lược matching: Path.match() cho patterns đơn giản
(không có **) và fnmatch.fnmatch() với prefix "*/"
cho patterns có **. Kết hợp hai lớp này đảm bảo cả path tuyệt đối
lẫn path tương đối (sau khi resolve) đều được kiểm tra đúng.
CVE-2026-40515 — root path bypass và fix
permissions/checker.py — CVE-2026-40515: glob root bypass
{`
# CVE-2026-40515: root path bypass
# Before fix:
def old_check(path: str) -> bool:
return any(fnmatch.fnmatch(path, p) for p in SENSITIVE_PATTERNS)
# Attack: old_check("/") → False (no pattern matches "/")
# But glob("/") returns everything including ~/.ssh/id_rsa
# After fix: check the FILES being accessed, not the search root
def new_check_glob_result(found_paths: list[str]) -> list[str]:
return [p for p in found_paths if not is_sensitive_path(p)]
`}
Lỗi gốc: check được áp dụng lên tham số tìm kiếm ("/") thay vì
các file thực sự được trả về. Fix chuyển check sang kết quả của glob — lọc
ra mọi path nhạy cảm trong danh sách kết quả trước khi trả về agent.
CVE-2026-40502 — path traversal không normalize
permissions/checker.py — CVE-2026-40502: path traversal
{`
# CVE-2026-40502: file_path not normalized before check
# Attack payload: "/../../../.ssh/id_rsa"
# Before fix:
def old_is_sensitive(path: str) -> bool:
return any(fnmatch.fnmatch(path, p) for p in SENSITIVE_PATTERNS)
# old_is_sensitive("/../../../.ssh/id_rsa") → False
# fnmatch sees literal "/../../../.ssh/id_rsa", not "/home/user/.ssh/id_rsa"
# After fix (current code):
def is_sensitive_path(path: str | Path) -> bool:
resolved = Path(path).resolve() # ← resolve FIRST
resolved_str = str(resolved) # → "/home/user/.ssh/id_rsa"
for pattern in SENSITIVE_PATTERNS:
if resolved.match(pattern): # matches "*/.ssh/*" ✓
return True
return False
# is_sensitive_path("/../../../.ssh/id_rsa") → True ✓
`}Tương tác với các kỹ thuật khác Interaction
Failure modes Failures
Failure 1: Pattern miss — credential file mới không có trong list
Ví dụ: ~/.config/op/credentials (1Password CLI) không bị chặn
{`
# ~/.config/op/credentials — 1Password CLI credential
# Không nằm trong SENSITIVE_PATTERNS → không bị block
# Mitigations:
# 1. Thêm pattern mới vào SENSITIVE_PATTERNS khi phát hiện
# 2. Thêm wildcard rộng hơn: "*/.config/*/*credentials*"
# 3. Implement pluggable pattern list để operator có thể extend
# mà không cần fork OpenHarness
# SENSITIVE_PATTERNS hiện tại bao phủ: AWS, GCP, Azure, kube,
# SSH, GPG, .env, PEM/KEY — nhưng không phải mọi credential manager
`}Failure 2: Symlink escape — resolve() theo symlink đúng cách
Symlink attack: ln -s ~/.ssh/id_rsa /tmp/key.txt
{`
# Attack: tạo symlink trỏ đến sensitive file
# ln -s ~/.ssh/id_rsa /tmp/key.txt
# Thử bypass: is_sensitive_path("/tmp/key.txt")
# Path("/tmp/key.txt").resolve() → "/home/user/.ssh/id_rsa"
# (resolve() theo symlink đến đích thật)
# Kết quả: match "*/.ssh/*" → BLOCK ✓
# resolve() tự động follow symlinks — đây là behavior mong muốn.
# Symlink attack bị chặn vì check được áp dụng lên ĐÍCH thật,
# không phải tên symlink.
`}Failure 3: Archive bypass — tar không check files bên trong
Archive bypass: tar cf keys.tar ~/.ssh/ vẫn có thể thực thi
{`
# Attack: thay vì đọc trực tiếp, agent tạo archive
# bash_tool: "tar cf /tmp/keys.tar ~/.ssh/"
# T19 check: path argument của bash_tool là "/tmp/keys.tar" (output)
# hoặc check command string "tar cf /tmp/keys.tar ~/.ssh/"
# T20 layer 5 check command deny patterns — nhưng nếu pattern
# không cover "tar ... ~/.ssh/", command có thể pass
# Mitigations:
# - Thêm command deny pattern: r"tar\s+.*\.ssh" vào layer 5 (T20)
# - Block bash_tool hoàn toàn trong PLAN mode (T18)
# - Audit bash command args cho known archive tools
`}So sánh với các harness khác Compare
| Harness | Hardcoded sensitive paths | Pre-check trước mode | CVE patches | Symlink follow |
|---|---|---|---|---|
| OpenHarness | 12 patterns (*/.ssh/*, **/.env, **/*.pem...) | Có — unconditional layer 1 | CVE-2026-40515 + CVE-2026-40502 | Có (resolve()) |
| Claude Code | Có (danh sách tương tự) | Có | N/A (upstream) | Partial |
| Aider | Không có built-in | Không | N/A | Không |
| AutoGPT | Không có built-in | Không | N/A | Không |
OpenHarness và Claude Code đều có sensitive path list, nhưng OpenHarness vá thêm hai CVE liên quan đến bypass qua root path và path traversal chưa normalize. Aider và AutoGPT không có bảo vệ built-in — operator phải tự implement nếu cần.
Implementation recipe Recipe
Minimal sensitive path protection — đủ dùng cho agent harness mới, bao gồm cả fix cho CVE-2026-40515 và CVE-2026-40502:
sensitive_paths.py — minimal implementation
{`
import fnmatch
from pathlib import Path
SENSITIVE = [
"*/.ssh/*", "*/.aws/credentials", "*/.aws/config",
"*/.kube/config", "*/.gnupg/*", "**/.env", "**/*.pem", "**/*.key",
]
def is_sensitive(path_str: str) -> bool:
# CVE-2026-40502 fix: resolve() TRƯỚC khi check
resolved = Path(path_str).resolve()
s = str(resolved)
return any(
resolved.match(pat) or fnmatch.fnmatch(s, "*/" + pat)
for pat in SENSITIVE
)
def safe_read(path_str: str) -> str:
if is_sensitive(path_str):
raise PermissionError(f"Blocked: sensitive path {path_str}")
return Path(path_str).read_text()
# CVE-2026-40515 fix: filter kết quả glob, không chỉ filter tham số
def safe_glob(root: str, pattern: str) -> list[str]:
all_results = list(Path(root).glob(pattern))
return [str(p) for p in all_results if not is_sensitive(str(p))]
# Usage:
# safe_read("/../../../.ssh/id_rsa") → PermissionError ✓
# safe_glob("/", "**/*") → trả về list đã lọc sensitive files ✓
`}Tham khảo Refs
- Anthropic — Claude Code Security model · Sensitive file protection, path checks trong Claude Code upstream
- NVD — Path traversal via unresolved symlinks · Lớp CVE tương tự: path traversal qua symlink và non-normalized paths
- OWASP — Path Traversal · Taxonomy tấn công path traversal và các bypass pattern phổ biến
- Python docs — Path.resolve() · Tại sao resolve() là bước bắt buộc: follow symlinks + collapse ..
- Python docs — fnmatch · Glob pattern matching — giới hạn của fnmatch với ** patterns
- HumanLayer — Harness Engineering for Coding Agents · Defensive layers trong coding agent harness: path protection best practices