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:
parent
eb91867340
commit
1a109b3263
@ -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]"),
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user