fix(security): H3 github_pat_ redaction + M4 atomic token write (audit cycle 10)

H3 (compliance.py): GitHub fine-grained PATs use the github_pat_ prefix
with an 82-character alphanumeric+underscore suffix — different from
classic tokens (36 chars). Add the missing pattern to _PII_PATTERNS so
fine-grained PATs are redacted in compliance logs alongside classic tokens.

M4 (platform_auth.py): Replace write_text()+chmod() in save_token() with
os.open(O_WRONLY|O_CREAT|O_TRUNC, 0o600) + os.write(). The old approach
had a TOCTOU window where a concurrent reader could access the token file
before chmod restricted permissions. os.open with explicit mode creates the
file with 0600 permissions atomically in a single syscall.

H2 (a2a_client.py): Already fixed in commit 6c78962 (Cycle 5); no-op.

Tests: 1136 passed, 2 skipped (workspace-template pytest suite)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dev Lead Agent 2026-04-14 09:34:27 +00:00
parent eb91867340
commit 1a109b3263
2 changed files with 16 additions and 8 deletions

View File

@ -257,8 +257,10 @@ _PII_PATTERNS: list[tuple[re.Pattern[str], str]] = [
(re.compile(r"\b(?:sk|pk|api|secret|token|auth)[-_][A-Za-z0-9_\-]{20,}\b", re.I), "[REDACTED:api_key]"),
# AWS Access Key IDs
(re.compile(r"\bAKIA[0-9A-Z]{16}\b"), "[REDACTED:aws_key]"),
# GitHub personal access tokens
# GitHub personal access tokens — classic format (36-char alphanumeric suffix)
(re.compile(r"\bghp_[A-Za-z0-9]{36}\b"), "[REDACTED:github_token]"),
# GitHub personal access tokens — fine-grained format (82-char alphanumeric+underscore suffix)
(re.compile(r"\bgithub_pat_[A-Za-z0-9_]{82}\b"), "[REDACTED:github_token]"),
# Email addresses
(re.compile(r"\b[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}\b"), "[REDACTED:email]"),
]

View File

@ -58,7 +58,12 @@ def get_token() -> str | None:
def save_token(token: str) -> None:
"""Persist a newly-issued token. Creates the file with 0600 mode.
"""Persist a newly-issued token. Creates the file with 0600 mode atomically.
Uses ``os.open(O_CREAT, 0o600)`` so the file is never world-readable,
even transiently. The previous ``write_text()`` + ``chmod()`` approach
had a TOCTOU window where a concurrent reader could access the token
between the two syscalls (M4 flagged in security audit cycle 10).
Idempotent if an identical token is already on disk we skip the
write so we don't churn the file's mtime or trigger spurious
@ -71,13 +76,14 @@ def save_token(token: str) -> None:
return
path = _token_file()
path.parent.mkdir(parents=True, exist_ok=True)
# Write + chmod before assigning — if the chmod fails we don't want
# a world-readable copy of the token sitting around.
path.write_text(token)
# O_CREAT | O_WRONLY | O_TRUNC with mode=0o600 atomically creates (or
# truncates) the file with restricted permissions in a single syscall,
# eliminating the TOCTOU window.
fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
try:
os.chmod(path, 0o600)
except OSError as exc:
logger.warning("platform_auth: chmod 0600 on %s failed: %s", path, exc)
os.write(fd, token.encode())
finally:
os.close(fd)
_cached_token = token