From 3321d2e266a05bbcadd6e6fc85a63ece94d4e1b3 Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Thu, 9 Apr 2026 06:57:23 +0200 Subject: [PATCH] security: reduce JWT expiry to 24h and add jti claim Tokens previously lasted 30 days with no revocation path. Expiry is now 24 hours and every token carries a unique jti (UUID hex) to support future revocation checks. --- app/auth/jwt.py | 4 +++- tests/test_security.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/auth/jwt.py b/app/auth/jwt.py index 7696ef6..54c724d 100644 --- a/app/auth/jwt.py +++ b/app/auth/jwt.py @@ -1,6 +1,7 @@ """JWT token creation and verification for API auth.""" import os +import uuid from datetime import datetime, timedelta, timezone from typing import Optional @@ -24,7 +25,7 @@ elif len(SECRET_KEY) < 32 and os.environ.get("TESTING", "").lower() not in ("1", ) ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_HOURS = 24 * 30 # 30 days +ACCESS_TOKEN_EXPIRE_HOURS = 24 # 24 hours def create_access_token( @@ -42,6 +43,7 @@ def create_access_token( "role": role, "exp": expire, "iat": datetime.now(timezone.utc), + "jti": uuid.uuid4().hex, } return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM) diff --git a/tests/test_security.py b/tests/test_security.py index d982e1c..aeb038f 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -111,6 +111,18 @@ class TestScriptSandbox: # Should still run but without access to dangerous modules assert resp.status_code == 200 + def test_sandbox_cannot_import_httpx(self, client): + """httpx must be blocked — either by pattern check (400) or + ModuleNotFoundError at runtime due to stripped VIRTUAL_ENV/PYTHONPATH (200 with non-zero exit).""" + c, token = client + resp = c.post("/api/scripts/run", json={ + "source": "import httpx\nprint('pwned')", + }, headers=_headers(token)) + # Static pattern check should reject it outright + assert resp.status_code == 400 or ( + resp.status_code == 200 and resp.json()["exit_code"] != 0 + ) + # ---- SQL Query Security ---- @@ -213,6 +225,26 @@ class TestAuthSecurity: assert resp.status_code == 401 +# ---- JWT Claims ---- + +class TestJwtClaims: + def test_jwt_contains_jti_claim(self): + """Token payload must include a jti claim with at least 16 hex chars.""" + os.environ.setdefault("TESTING", "1") + from app.auth.jwt import create_access_token, verify_token + token = create_access_token("u1", "user@test.com", "analyst") + payload = verify_token(token) + assert payload is not None + assert "jti" in payload + assert len(payload["jti"]) >= 16 + + def test_jwt_expiry_is_24_hours(self): + """ACCESS_TOKEN_EXPIRE_HOURS must be 24 (not 30*24).""" + os.environ.setdefault("TESTING", "1") + from app.auth import jwt as jwt_module + assert jwt_module.ACCESS_TOKEN_EXPIRE_HOURS == 24 + + # ---- JWT Secret Hardening ---- class TestJwtSecretHardening: