feat: harden OAuth state secret validation, DCR file permissions, and policy defaults
docker / test (pull_request) Successful in 24s
lint / lint (pull_request) Successful in 37s
lint / lint (push) Successful in 1m26s
test / test (push) Successful in 1m40s
test / test (pull_request) Successful in 34s
docker / lint (pull_request) Successful in 1m59s
docker / docker-test (pull_request) Successful in 14s
docker / docker-publish (pull_request) Has been skipped
docker / test (pull_request) Successful in 24s
lint / lint (pull_request) Successful in 37s
lint / lint (push) Successful in 1m26s
test / test (push) Successful in 1m40s
test / test (pull_request) Successful in 34s
docker / lint (pull_request) Successful in 1m59s
docker / docker-test (pull_request) Successful in 14s
docker / docker-publish (pull_request) Has been skipped
- Enforce 32-char minimum on OAUTH_STATE_SECRET at startup (config.py) - Write DCR client registry with owner-only (0o600) permissions before atomic replace - Flip policy.yaml default write action from allow → deny - Add CLAUDE.md with architecture, commands, and AGENTS.md contract summary - Add .pre-commit-config.yaml mirroring `make lint` checks - Update .gitignore: add .venv, .claude, .mypy_cache, .ruff_cache, .coverage.* - Extend docs: audit log rotation guidance, OAUTH_STATE_SECRET and DCR_STORAGE_PATH notes - Tests: short-secret rejection, 32-char acceptance, POSIX permission check for DCR store Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -323,6 +323,12 @@ class Settings(BaseSettings):
|
||||
"OAUTH_STATE_SECRET is required when OAUTH_MODE=true so the OAuth "
|
||||
"proxy state parameter can be HMAC-signed and verified."
|
||||
)
|
||||
# A short secret weakens the HMAC; require the same 32-char floor as API keys.
|
||||
if len(self.oauth_state_secret.strip()) < 32:
|
||||
raise ValueError(
|
||||
"OAUTH_STATE_SECRET must be at least 32 characters long "
|
||||
"(e.g. `openssl rand -hex 32`)."
|
||||
)
|
||||
else:
|
||||
# Standard API key mode: require bot token and at least one API key.
|
||||
if not self.gitea_token.strip():
|
||||
|
||||
@@ -6,6 +6,7 @@ import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
from fnmatch import fnmatchcase
|
||||
@@ -282,6 +283,13 @@ class OAuthClientRegistry:
|
||||
}
|
||||
tmp_path = self.storage_path.with_suffix(self.storage_path.suffix + ".tmp")
|
||||
tmp_path.write_text(json.dumps(payload, sort_keys=True, indent=2), encoding="utf-8")
|
||||
# Registration records hold client-secret hashes and metadata; restrict to the
|
||||
# owning user before the atomic replace so the file is never briefly world-readable.
|
||||
# chmod is a no-op on platforms without POSIX permission bits (e.g. Windows).
|
||||
try:
|
||||
os.chmod(tmp_path, 0o600)
|
||||
except (OSError, NotImplementedError):
|
||||
pass
|
||||
tmp_path.replace(self.storage_path)
|
||||
|
||||
def get(self, client_id: str) -> OAuthClientRecord | None:
|
||||
|
||||
Reference in New Issue
Block a user