fix: harden evidence secret file handoff

This commit is contained in:
2026-08-09 19:27:52 +02:00
parent e50aad7e41
commit 3b9681a63a
4 changed files with 83 additions and 24 deletions
@@ -223,6 +223,37 @@ def test_signed_http_rejects_reordered_extra_mismatch_userinfo_and_duplicate_pro
assert_no_canaries(caught.value)
def test_s3_rejects_inline_credentials_and_never_discloses_them(tmp_path):
path = write_config(tmp_path, {
"type": "s3", "bucket": "clinical-evidence",
"access_key": ACCESS_CANARY,
"secret_key": SECRET_CANARY,
"session_token": TOKEN_CANARY,
})
with pytest.raises(ConfigError) as caught:
load_config(path)
assert "file" in str(caught.value).lower()
assert_no_canaries(caught.value)
assert_no_canaries("".join(traceback.format_exception(caught.value)))
def test_s3_scalar_secret_files_are_bounded(tmp_path):
access = tmp_path / "oversized-access-key"
access.write_bytes(b"A" * (64 * 1024 + 1))
secret = tmp_path / "secret-key"
secret.write_text("bounded-secret")
path = write_config(tmp_path, {
"type": "s3", "bucket": "clinical-evidence",
"access_key_file": str(access), "secret_key_file": str(secret),
})
with pytest.raises(ConfigError) as caught:
load_config(path)
assert "secret file" in str(caught.value).lower()
assert "bounded-secret" not in str(caught.value)
def test_s3_ambient_and_static_file_credentials_are_secret_typed(tmp_path):
ambient = load_config(write_config(tmp_path, {
"type": "s3", "bucket": "clinical-evidence", "prefix": "published/",
+18 -7
View File
@@ -49,13 +49,17 @@ def _expand_env(value: Any) -> Any:
_MAX_SIGNED_URL_FILE_BYTES = 1024 * 1024
def _resolve_http_signed_url_files(value: Any) -> Any:
def _resolve_evidence_secret_files(value: Any) -> Any:
"""Resolve only signed HTTP URL arrays, keeping their values out of public errors."""
if isinstance(value, dict):
resolved = {
key: _resolve_http_signed_url_files(item)
key: _resolve_evidence_secret_files(item)
for key, item in value.items()
}
if resolved.get("type") == "s3" and any(
name in resolved for name in ("access_key", "secret_key", "session_token")
):
raise ConfigError("S3 Evidence credentials require *_file references")
if resolved.get("type") != "http" or "signed_urls_file" not in resolved:
return resolved
if "urls" in resolved:
@@ -94,10 +98,13 @@ def _resolve_http_signed_url_files(value: Any) -> Any:
resolved["urls"] = parsed
return resolved
if isinstance(value, list):
return [_resolve_http_signed_url_files(item) for item in value]
return [_resolve_evidence_secret_files(item) for item in value]
return value
_MAX_SCALAR_SECRET_FILE_BYTES = 64 * 1024
def _resolve_secret_files(value: Any) -> Any:
if isinstance(value, dict):
resolved = {key: _resolve_secret_files(item) for key, item in value.items()}
@@ -109,9 +116,13 @@ def _resolve_secret_files(value: Any) -> Any:
raise ConfigError(f"{secret_name} and {file_name} are mutually exclusive")
path = Path(resolved.pop(file_name))
try:
secret = path.read_text()
except (OSError, UnicodeError) as exc:
raise ConfigError(f"Cannot read secret file: {path}") from exc
with path.open("rb") as stream:
payload = stream.read(_MAX_SCALAR_SECRET_FILE_BYTES + 1)
if len(payload) > _MAX_SCALAR_SECRET_FILE_BYTES:
raise OSError
secret = payload.decode("utf-8")
except (OSError, UnicodeError):
raise ConfigError(f"Cannot read secret file: {path}") from None
if not secret or any(char.isspace() for char in secret) or "\x00" in secret:
raise ConfigError(f"Invalid secret file: {path}")
resolved[secret_name] = secret
@@ -532,7 +543,7 @@ def load_config(path: Path) -> Config:
raise ConfigError(f"Configurazione YAML non valida: {path}") from exc
if not isinstance(raw, dict):
raise ConfigError(f"Configurazione non valida (atteso un mapping YAML): {path}")
expanded = _resolve_secret_files(_resolve_http_signed_url_files(_expand_env(raw)))
expanded = _resolve_secret_files(_resolve_evidence_secret_files(_expand_env(raw)))
_validate_internal_embedding_contract(expanded, path)
_validate_internal_vector_contract(expanded, path)
translated, used_legacy = translate_legacy_config(expanded)