@cryptotaxi247 / CoPilot / commits / 4640511a

fix(auth): remove hardcoded JWT secret fallback (GHSA-4gxj-hw3c-3x2x) (#814)

* fix(auth): remove hardcoded JWT secret fallback, fail fast on unset/compromised Addresses GHSA-4gxj-hw3c-3x2x. The JWT signing secret had a hardcoded fallback matching the value shipped in .env.example, so any deployment without JWT_SECRET explicitly set signed tokens with a publicly known value. A second copy of the same fallback lived in the TOTP service. - backend/app/auth/utils.py: introduce _load_jwt_secret() that raises RuntimeError at import time if JWT_SECRET is unset or equals the known-compromised default disclosed in the advisory. - backend/app/auth/services/totp.py: remove the duplicate fallback and derive the TOTP Fernet fallback key from AuthHandler.secret. - .env.example: replace the published secret with REPLACE_ME and point operators at `openssl rand -base64 32`. SSO state signing (app/auth/services/sso.py) reads AuthHandler().secret and inherits the validation with no code change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * precommit-fixes --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

taylor_socfortress committed Apr 24, 2026 at 12:56 UTC 4640511a0cf2e7b144a71375b5b349a8318cb186
3 files changed +26 -5
.env.example
+3 -2
@@ -2,9 +2,10 @@
2 SERVER_IP=0.0.0.0
3
4 # SECURITY: JWT secret for token signing.
5 -# ⚠️ The default value below is publicly known — change it before any deployment!
5 +# REQUIRED — the application will refuse to start if this is unset or set to the
6 +# known-compromised default disclosed in GHSA-4gxj-hw3c-3x2x.
7 # Generate a unique secret with: openssl rand -base64 32
7 -JWT_SECRET=bL4unrkoxtFs1MT6A7Ns2yMLkduyuqrkTxDV9CjlbNc=
8 +JWT_SECRET=REPLACE_ME
9
10 # SECURITY: Dedicated secret for signing OAuth2 state tokens (SSO flows).
11 # If not set, falls back to JWT_SECRET — set this to isolate SSO state signing.
backend/app/auth/services/totp.py
+2 -2
@@ -21,6 +21,7 @@ from sqlalchemy.orm.attributes import flag_modified
21 from sqlmodel import select
22
23 from app.auth.models.totp import UserTOTP
24 +from app.auth.utils import AuthHandler
25 from app.db.db_session import async_engine
26
27 # Prefer a dedicated TOTP_ENCRYPTION_KEY (proper Fernet key, separate from JWT).
@@ -30,8 +31,7 @@ _totp_enc_key = os.environ.get("TOTP_ENCRYPTION_KEY")
31 if _totp_enc_key:
32 _fernet_key = _totp_enc_key.encode()
33 else:
33 - _raw_secret = os.environ.get("JWT_SECRET", "bL4unrkoxtFs1MT6A7Ns2yMLkduyuqrkTxDV9CjlbNc=")
34 - _fernet_key = base64.urlsafe_b64encode(hashlib.sha256(_raw_secret.encode()).digest())
34 + _fernet_key = base64.urlsafe_b64encode(hashlib.sha256(AuthHandler.secret.encode()).digest())
35 _fernet = Fernet(_fernet_key)
36
37 _pwd_ctx = CryptContext(schemes=["bcrypt"])
backend/app/auth/utils.py
+21 -1
@@ -13,6 +13,26 @@ from passlib.context import CryptContext
13 from app.auth.services.universal import find_user
14 from app.auth.services.universal import get_role
15
16 +# Known-compromised value published in prior versions of .env.example and as a
17 +# hardcoded fallback. Refuse to boot if it reappears, regardless of source.
18 +_KNOWN_COMPROMISED_JWT_SECRET = "bL4unrkoxtFs1MT6A7Ns2yMLkduyuqrkTxDV9CjlbNc="
19 +
20 +
21 +def _load_jwt_secret() -> str:
22 + secret = os.environ.get("JWT_SECRET")
23 + if not secret:
24 + raise RuntimeError(
25 + "JWT_SECRET environment variable is not set. Generate a secure value "
26 + "with `openssl rand -base64 32` and set it before starting the application.",
27 + )
28 + if secret == _KNOWN_COMPROMISED_JWT_SECRET:
29 + raise RuntimeError(
30 + "JWT_SECRET is set to the known-compromised default value disclosed in "
31 + "GHSA-4gxj-hw3c-3x2x. Generate a new secret with `openssl rand -base64 32` "
32 + "and update your environment.",
33 + )
34 + return secret
35 +
36
37 class AuthHandler:
38 security = OAuth2PasswordBearer(
@@ -25,7 +45,7 @@ class AuthHandler:
45 },
46 )
47 pwd_context = CryptContext(schemes=["bcrypt"])
28 - secret = os.environ.get("JWT_SECRET", "bL4unrkoxtFs1MT6A7Ns2yMLkduyuqrkTxDV9CjlbNc=")
48 + secret = _load_jwt_secret()
49
50 def get_password_hash(self, password):
51 return self.pwd_context.hash(password)