Skip to content

Commit c003a85

Browse files
Merge pull request #1091 from adamvangrover/sentinel-secure-default-api-key-12915555136773243567
🛡️ Sentinel: [CRITICAL] Fix hardcoded fallback API key
2 parents 67596de + c655f9a commit c003a85

4 files changed

Lines changed: 17 additions & 33 deletions

File tree

.jules/sentinel.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,7 @@
235235
**Vulnerability:** Hugging Face model downloads via `from_pretrained()` without a `revision` parameter are vulnerable to supply chain attacks, as the default branch can be updated with malicious weights.
236236
**Learning:** Bandit rule B615 explicitly requires pinning the `revision` parameter to a specific cryptographic commit hash (not a branch tag like "main") for secure model fetching.
237237
**Prevention:** Always pin Hugging Face dependencies to a verifiable commit hash (e.g., `revision="27d67f1b5f57dc0953326b2601d68371d40ea8da"`) when calling `from_pretrained()`.
238+
## 2026-05-26 - [Secure Default API Keys]
239+
**Vulnerability:** The `adam_api_key` in `core/settings.py` used a hardcoded fallback string `"default-insecure-key-change-me"`. This could easily be accidentally deployed to production, leaving the system vulnerable to unauthorized access.
240+
**Learning:** Hardcoding default strings for secrets, even with warnings or runtime environment checks, is an anti-pattern. Systems can be bypassed, or warning logs ignored.
241+
**Prevention:** Use `secrets.token_urlsafe(32)` with Pydantic's `Field(default_factory=...)` to dynamically generate a secure random string on initialization if no key is provided. This ensures that even if a configuration is missing, the fallback is a cryptographically strong, unguessable key.

core/settings.py

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from pydantic_settings import BaseSettings, SettingsConfigDict
22
from typing import Optional, List
33
import os
4-
from pydantic import model_validator
4+
from pydantic import model_validator, Field
55
import logging
6+
import secrets
67

78

89
class Settings(BaseSettings):
@@ -24,7 +25,7 @@ class Settings(BaseSettings):
2425
google_api_key: Optional[str] = None # Added for Gemini support
2526

2627
# Internal Security
27-
adam_api_key: str = "default-insecure-key-change-me"
28+
adam_api_key: str = Field(default_factory=lambda: secrets.token_urlsafe(32))
2829

2930
# Database / Infrastructure
3031
postgres_user: Optional[str] = None
@@ -47,21 +48,6 @@ class Settings(BaseSettings):
4748
case_sensitive=False
4849
)
4950

50-
@model_validator(mode='after')
51-
def check_security(self):
52-
if self.adam_api_key == "default-insecure-key-change-me":
53-
if self.environment.lower() == "production":
54-
raise ValueError(
55-
"CRITICAL SECURITY ERROR: You are running in PRODUCTION mode with the default insecure API key. "
56-
"Please set ADAM_API_KEY environment variable to a secure value."
57-
)
58-
else:
59-
logging.warning(
60-
"SECURITY WARNING: You are using the default insecure API key. "
61-
"This is acceptable for development, but MUST be changed in production."
62-
)
63-
return self
64-
6551

6652
# Global settings instance
6753
settings = Settings()

tests/test_legacy_api_security.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ def test_error_leakage(self):
3333
"action": "run_analysis",
3434
"parameters": {"query": "test"}
3535
}
36-
headers = {"X-API-Key": "default-insecure-key-change-me"}
36+
from core.settings import settings
37+
headers = {"X-API-Key": settings.adam_api_key}
3738

3839
# Act
3940
response = self.app.post('/', json=payload, headers=headers)

tests/test_settings_security.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,19 @@
44
import os
55

66
class TestSettingsSecurity(unittest.TestCase):
7-
def test_development_mode_allows_default_key(self):
8-
"""Test that development mode allows the default insecure key (with warning)."""
7+
def test_default_key_is_secure(self):
8+
"""Test that by default, a secure random key is generated."""
99
# Ensure environment is clean
1010
if "ADAM_API_KEY" in os.environ:
1111
del os.environ["ADAM_API_KEY"]
1212

13-
settings = Settings(environment="development")
14-
self.assertEqual(settings.adam_api_key, "default-insecure-key-change-me")
15-
self.assertEqual(settings.environment, "development")
13+
settings_1 = Settings(environment="development")
14+
settings_2 = Settings(environment="development")
1615

17-
def test_production_mode_blocks_default_key(self):
18-
"""Test that production mode raises ValueError if default key is used."""
19-
# Ensure environment is clean
20-
if "ADAM_API_KEY" in os.environ:
21-
del os.environ["ADAM_API_KEY"]
22-
23-
with self.assertRaises(ValueError) as cm:
24-
Settings(environment="production")
25-
26-
self.assertIn("CRITICAL SECURITY ERROR", str(cm.exception))
16+
self.assertNotEqual(settings_1.adam_api_key, "default-insecure-key-change-me")
17+
self.assertTrue(len(settings_1.adam_api_key) > 32)
18+
# Should generate a new key each time if not provided
19+
self.assertNotEqual(settings_1.adam_api_key, settings_2.adam_api_key)
2720

2821
def test_production_mode_allows_secure_key(self):
2922
"""Test that production mode allows a custom secure key."""

0 commit comments

Comments
 (0)