From 74a6551237cd1b3db6a0c9c4351828f769ef0e6e Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Sat, 21 Feb 2026 13:57:23 -0300 Subject: [PATCH] Redact quoted JSON secret tokens in processing logs --- backend/app/models/processing_log.py | 3 ++ backend/tests/test_security_controls.py | 68 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/backend/app/models/processing_log.py b/backend/app/models/processing_log.py index 25b17dc..028dc56 100644 --- a/backend/app/models/processing_log.py +++ b/backend/app/models/processing_log.py @@ -28,6 +28,9 @@ SENSITIVE_KEY_MARKERS = ( "cookie", ) SENSITIVE_TEXT_PATTERNS = ( + re.compile(r"(?i)[\"']authorization[\"']\s*:\s*[\"']bearer\s+[^\"']+[\"']"), + re.compile(r"(?i)[\"']bearer[\"']\s*:\s*[\"'][^\"']+[\"']"), + re.compile(r"(?i)[\"'](?:api[_-]?key|token|secret|password)[\"']\s*:\s*[\"'][^\"']+[\"']"), re.compile(r"(?i)\bauthorization\b\s*[:=]\s*bearer\s+[a-z0-9._~+/\-]+=*"), re.compile(r"(?i)\bbearer\s+[a-z0-9._~+/\-]+=*"), re.compile(r"\b[a-z0-9_-]{8,}\.[a-z0-9_-]{8,}\.[a-z0-9_-]{8,}\b", flags=re.IGNORECASE), diff --git a/backend/tests/test_security_controls.py b/backend/tests/test_security_controls.py index 29092b4..e47431b 100644 --- a/backend/tests/test_security_controls.py +++ b/backend/tests/test_security_controls.py @@ -237,6 +237,30 @@ class ProcessingLogRedactionTests(unittest.TestCase): self.assertNotIn(bearer_token, sanitized_text) self.assertNotIn(jwt_token, sanitized_text) + def test_text_redaction_removes_json_formatted_secret_values(self) -> None: + """JSON-formatted quoted secrets are fully removed from redacted log text.""" + + api_key_secret = "json-api-key-secret" + token_secret = "json-token-secret" + authorization_secret = "json-auth-secret" + bearer_secret = "json-bearer-secret" + json_text = ( + "{" + f"\"api_key\":\"{api_key_secret}\"," + f"\"token\":\"{token_secret}\"," + f"\"authorization\":\"Bearer {authorization_secret}\"," + f"\"bearer\":\"{bearer_secret}\"" + "}" + ) + sanitized = sanitize_processing_log_text(json_text) + self.assertIsNotNone(sanitized) + sanitized_text = sanitized or "" + self.assertIn("[REDACTED]", sanitized_text) + self.assertNotIn(api_key_secret, sanitized_text) + self.assertNotIn(token_secret, sanitized_text) + self.assertNotIn(authorization_secret, sanitized_text) + self.assertNotIn(bearer_secret, sanitized_text) + def test_response_schema_applies_redaction_to_existing_entries(self) -> None: """API schema validators redact sensitive fields from legacy stored rows.""" @@ -268,6 +292,50 @@ class ProcessingLogRedactionTests(unittest.TestCase): self.assertNotIn(bearer_token, response.prompt_text or "") self.assertNotIn(jwt_token, response.response_text or "") + def test_response_schema_redacts_json_formatted_secret_values(self) -> None: + """Response schema redacts quoted JSON secret forms from legacy text fields.""" + + api_key_secret = "legacy-json-api-key" + token_secret = "legacy-json-token" + authorization_secret = "legacy-json-auth" + bearer_secret = "legacy-json-bearer" + prompt_text = ( + "{" + f"\"api_key\":\"{api_key_secret}\"," + f"\"token\":\"{token_secret}\"" + "}" + ) + response_text = ( + "{" + f"\"authorization\":\"Bearer {authorization_secret}\"," + f"\"bearer\":\"{bearer_secret}\"" + "}" + ) + + response = ProcessingLogEntryResponse.model_validate( + { + "id": 2, + "created_at": datetime.now(UTC), + "level": "info", + "stage": "summary", + "event": "response", + "document_id": None, + "document_filename": "sample-json.txt", + "provider_id": "provider", + "model_name": "model", + "prompt_text": prompt_text, + "response_text": response_text, + "payload_json": {"trace_id": "trace-2"}, + } + ) + + self.assertIn("[REDACTED]", response.prompt_text or "") + self.assertIn("[REDACTED]", response.response_text or "") + self.assertNotIn(api_key_secret, response.prompt_text or "") + self.assertNotIn(token_secret, response.prompt_text or "") + self.assertNotIn(authorization_secret, response.response_text or "") + self.assertNotIn(bearer_secret, response.response_text or "") + if __name__ == "__main__": unittest.main()