From 4aa97c23d29f2abed07a64c4a2d72915d329676b Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Thu, 9 Apr 2026 06:54:29 +0200 Subject: [PATCH] fix: raise RuntimeError on missing JWT_SECRET_KEY in non-test environments Prevents production deployments from silently using a hardcoded default secret. TESTING=1 still resolves to a built-in test key so the existing test suite is unaffected. Adds a test that verifies the RuntimeError is raised when neither JWT_SECRET_KEY nor TESTING is set. --- app/auth/jwt.py | 16 ++++++++++++---- tests/test_security.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/app/auth/jwt.py b/app/auth/jwt.py index a09f4e4..7696ef6 100644 --- a/app/auth/jwt.py +++ b/app/auth/jwt.py @@ -6,12 +6,20 @@ from typing import Optional import jwt -SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "dev-jwt-secret-change-in-production") +SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "") -import warnings as _warnings -if len(SECRET_KEY) < 32 and os.environ.get("TESTING", "").lower() not in ("1", "true"): +if not SECRET_KEY: + if os.environ.get("TESTING", "").lower() in ("1", "true"): + SECRET_KEY = "test-jwt-secret-key-minimum-32-chars!!" + else: + raise RuntimeError( + "JWT_SECRET_KEY environment variable is required. " + "Generate one: python -c \"import secrets; print(secrets.token_hex(32))\"" + ) +elif len(SECRET_KEY) < 32 and os.environ.get("TESTING", "").lower() not in ("1", "true"): + import warnings as _warnings _warnings.warn( - f"JWT_SECRET_KEY is {len(SECRET_KEY)} chars — minimum 32 recommended for production", + f"JWT_SECRET_KEY is {len(SECRET_KEY)} chars — minimum 32 recommended", UserWarning, stacklevel=2, ) diff --git a/tests/test_security.py b/tests/test_security.py index 987dcc8..1da641b 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -1,6 +1,8 @@ """Security tests — sandbox escapes, SQL injection, access control.""" +import importlib import os +import sys import pytest from fastapi.testclient import TestClient @@ -190,3 +192,32 @@ class TestAuthSecurity: c, _ = client resp = c.get("/api/scripts") assert resp.status_code == 401 + + +# ---- JWT Secret Hardening ---- + +class TestJwtSecretHardening: + def test_raises_without_jwt_secret_in_non_test_env(self): + """Module-level code must raise RuntimeError when JWT_SECRET_KEY is absent + and TESTING is not set, preventing accidental production deploys with no secret.""" + saved_key = os.environ.pop("JWT_SECRET_KEY", None) + saved_testing = os.environ.pop("TESTING", None) + # Eject any cached module so the re-import re-executes module-level code + sys.modules.pop("app.auth.jwt", None) + try: + with pytest.raises(RuntimeError, match="JWT_SECRET_KEY environment variable is required"): + importlib.import_module("app.auth.jwt") + finally: + # Restore environment before re-importing so the module loads cleanly + if saved_key is not None: + os.environ["JWT_SECRET_KEY"] = saved_key + if saved_testing is not None: + os.environ["TESTING"] = saved_testing + # If neither was set (bare test run), use TESTING flag so reload works + if saved_key is None and saved_testing is None: + os.environ["TESTING"] = "1" + sys.modules.pop("app.auth.jwt", None) + importlib.import_module("app.auth.jwt") + # Clean up the temporary TESTING flag if we added it + if saved_key is None and saved_testing is None: + os.environ.pop("TESTING", None)