← openharness report

T19 — Built-in sensitive path protection

OpenHarness hardcode 12 glob patterns bao phủ các credential file phổ biến nhất — SSH keys, AWS/GCP/Azure credentials, kubeconfig, GPG, .env, PEM/KEY files. Check này chạy unconditionally trước mọi layer khác, bao gồm cả FULL_AUTO mode.
Nhóm: E — Permission & SafetyFile: permissions/checker.py:14-37Patterns: 12 glob patternsID: E.2

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.

Punchline kiến trúc: "Sensitive path protection is the only unconditional defense in OpenHarness — it runs before mode check, before allow/deny rules, before everything." Đây là "safety floor" không thể bị cấu hình sai hay vô tình tắt đi.
CVE-2026-40515 (glob root bypass): "CVE-2026-40515: passing path='/' to glob tool bypassed all path-based checks. Fix: normalize before matching." Kẻ tấn công truyền 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.
CVE-2026-40502 (path traversal không normalize): "CVE-2026-40502: file_path parameter was not resolved to absolute path before checking. Fix: Path(file_path).resolve() first." /../../../.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.
Fix chung cho cả hai CVE: "The fix for both CVEs is the same: resolve() before you check, never after." Gọi 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()

TS
{`
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

PY
{`
# 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

TS
{`
# 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

T19 (Sensitive path protection) trong permission pipeline: Request đến │ ▼ ┌─────────────────────────────────────────────────┐ │ Layer 1: T19 — is_sensitive_path(path)? │ ← T19 chạy ĐẦU TIÊN │ Nếu True → BLOCK ngay, không check gì thêm │ └───────────────────────────────┬─────────────────┘ │ False (không phải sensitive path) ▼ ┌─────────────────────────────────────────────────┐ │ Layer 2-5: Deny/Allow tool list, path rules, │ │ command deny patterns (T20) │ └───────────────────────────────┬─────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ Layer 6: T18 — Permission mode check │ │ FULL_AUTO / DEFAULT / PLAN │ └───────────────────────────────┬─────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ T21 — Async approval UI (nếu cần) │ └─────────────────────────────────────────────────┘ Quan hệ quan trọng: ├─ T18 (FULL_AUTO): KHÔNG bypass T19 — mode check là layer 6 ├─ T20 (6-layer eval): T19 chính là layer 1 của T20 ├─ T21 (Async approval): T19 block xảy ra trước khi approval UI hiện └─ T10 (Bash tool): Bash command chứa sensitive path args cũng được check

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

TS
{`
# ~/.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

TS
{`
# 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

TS
{`
# 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

HarnessHardcoded sensitive pathsPre-check trước modeCVE patchesSymlink follow
OpenHarness12 patterns (*/.ssh/*, **/.env, **/*.pem...)Có — unconditional layer 1CVE-2026-40515 + CVE-2026-40502Có (resolve())
Claude CodeCó (danh sách tương tự)N/A (upstream)Partial
AiderKhông có built-inKhôngN/AKhông
AutoGPTKhông có built-inKhôngN/AKhô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

PY
{`
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

Nguồn chính