Root cause
Other / insufficient public mechanism detail introduced
feat: Add unified FastAPI server with JWT authentication and modular architecture
How AI contributed: Direct introductionPublished Apr 1, 2026 in
jahlives/openssl_encryptAll 20 findings in this repository
The AI-written unified server accepted refresh tokens as Query parameters on the /refresh endpoints in openssl_encrypt_server/modules/telemetry/routes.py, so the token was carried in the URL rather than the request body. URLs are recorded by default, so the credential was copied into HTTP access logs, browser history and Referer headers sent to any third-party site the page requests. Anyone who reads those records can exchange the token for fresh access tokens, and the fix moves refresh token handling into a typed RefreshRequest body.
FastAPI read the refresh token from refresh_token: str = Query(...) on the /refresh route, and query strings are logged and forwarded as a matter of course, exposing the secret in access logs, browser history and Referer headers. The fix reads the token from a RefreshRequest model in the POST body instead.
Root cause
feat: Add unified FastAPI server with JWT authentication and modular architecture
Fix
security: Move refresh token from query params to POST body (GHSA-4rh7-jwg9-m28m)
Case facts
@@ -0,0 +1,198 @@+#!/usr/bin/env python3+"""+OpenSSL Encrypt Server - Main Entry Point++Unified FastAPI server for Keyserver and Telemetry modules.+"""++import logging+from contextlib import asynccontextmanager++from fastapi import FastAPI+from fastapi.middleware.cors import CORSMiddleware+import uvicorn++from .config import Settings, settings, validate_config+from .core.database import close_db, init_db, init_engine+from .core.exceptions import ServerException, server_exception_handler+from .modules import load_modules++# Configure logging+logging.basicConfig(+ level=getattr(logging, settings.log_level.upper()),+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",+)++logger = logging.getLogger(__name__)+++@asynccontextmanager+async def lifespan(app: FastAPI):+ """+ Lifespan event handler for FastAPI.++ Runs on startup and shutdown.+ """+ # Startup+ logger.info("=" * 60)+ logger.info(f"Starting {settings.app_name}...")+ logger.info(f"Version: {settings.version}")+ logger.info(f"Debug mode: {settings.debug}")+ logger.info("=" * 60)++ # Validate configuration+ try:+ validate_config(settings)+ logger.info("Configuration validated successfully")+ except ValueError as e:+ logger.error(f"Configuration error: {e}")+ raise++ # Import all models (must be done before init_db)+ if settings.keyserver_enabled:+ from .modules.keyserver import models as ks_models # noqa: F401+ if settings.telemetry_enabled:+ from .modules.telemetry import models as tm_models # noqa: F401++ # Initialize database+ try:+ database_url = settings.get_database_url()+ init_engine(database_url)+ await init_db()+ logger.info("Database initialized successfully")+ except Exception as e:+ logger.error(f"Failed to initialize database: {e}")+ raise++ # Check if liboqs is available+ try:+ import oqs++ logger.info("liboqs is available - PQC signature verification enabled")+ except ImportError:+ logger.warning(+ "liboqs not available - keyserver signature verification will fail. "+ "Install with: pip install liboqs-python"+ )++ # Load enabled modules+ try:+ loaded_modules = load_modules(app, settings)+ logger.info(f"Loaded modules: {', '.join(loaded_modules)}")+ except Exception as e:+ logger.error(f"Failed to load modules: {e}")+ raise++ logger.info("=" * 60)+ logger.info(f"Server started on {settings.server_host}:{settings.server_port}")+ logger.info("=" * 60)+@@ -0,0 +1,84 @@+#!/usr/bin/env python3+"""+Telemetry Pydantic Schemas++Request/response validation for telemetry endpoints.+"""++from datetime import datetime+from typing import Dict, List, Optional++from pydantic import BaseModel, Field+++class RegisterResponse(BaseModel):+ """Response for client registration"""++ client_id: str+ token: str+ expires_at: str # ISO 8601 datetime+ token_type: str = "Bearer"+++class TelemetryEventSchema(BaseModel):+ """Single telemetry event"""++ timestamp: str = Field(..., description="Event timestamp (ISO 8601)")+ operation: str = Field(..., description="Operation: encrypt or decrypt")+ mode: str = Field(..., description="Mode: symmetric or asymmetric")+ format_version: int = Field(..., description="File format version (4-8)")++ hash_algorithms: List[str] = Field(..., description="Hash algorithms used")+ kdf_algorithms: List[str] = Field(..., description="KDF algorithms used")+ kdf_parameters: Optional[Dict[str, Dict[str, int]]] = Field(+ default=None, description="KDF parameters"+ )++ encryption_algorithm: str = Field(..., description="Encryption algorithm")++ cascade_enabled: bool = Field(default=False, description="Cascade encryption enabled")+ cascade_cipher_count: Optional[int] = Field(+ default=None, description="Number of cascade ciphers"+ )++ pqc_kem_algorithm: Optional[str] = Field(default=None, description="PQC KEM algorithm")+ pqc_signing_algorithm: Optional[str] = Field(+ default=None, description="PQC signing algorithm"+ )++ hsm_plugin_used: Optional[str] = Field(default=None, description="HSM plugin name")++ success: bool = Field(default=True, description="Operation success")+ error_category: Optional[str] = Field(default=None, description="Error category if failed")+++class TelemetryBatchRequest(BaseModel):+ """Batch telemetry upload request"""++ events: List[TelemetryEventSchema] = Field(+ ..., min_length=1, max_length=1000, description="List of telemetry events (max 1000)"+ )+++class TelemetryBatchResponse(BaseModel):+ """Batch telemetry upload response"""++ received: int = Field(..., description="Number of events received")+ processed: int = Field(..., description="Number of events successfully processed")+++class StatsResponse(BaseModel):+ """Public statistics response"""++ total_operations: int+ total_clients: int+ algorithms: Dict[str, int]+ operations: Dict[str, int]+ success_rate: float+++class ErrorResponse(BaseModel):+ """Standard error response"""++ detail: str+ success: bool = False@@ -0,0 +1,124 @@+#!/usr/bin/env python3+"""+Telemetry API Routes++Endpoints:+- POST /api/v1/telemetry/register - Register new client (no auth)+- POST /api/v1/telemetry/events - Submit events (auth required)+- GET /api/v1/telemetry/stats - Get public statistics (no auth)+"""++import logging++from fastapi import APIRouter, Depends, Request, Security, status+from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer+from sqlalchemy.ext.asyncio import AsyncSession++from ...core.database import get_db+from .auth import get_telemetry_auth+from .schemas import (+ ErrorResponse,+ RegisterResponse,+ StatsResponse,+ TelemetryBatchRequest,+ TelemetryBatchResponse,+)+from .service import TelemetryService++logger = logging.getLogger(__name__)++router = APIRouter(prefix="/api/v1/telemetry", tags=["telemetry"])++security = HTTPBearer()+++# Dependency that lazily gets the auth instance+async def get_current_client(+ request: Request,+ credentials: HTTPAuthorizationCredentials = Security(security),+) -> str:+ """Get current authenticated client ID"""+ auth = get_telemetry_auth()+ payload = auth.verify_token(credentials.credentials)++ # Update last seen (fire and forget)+ try:+ await auth.update_last_seen(payload.sub)+ except Exception:+ pass++ return payload.sub++[email protected](+ "/register",+ response_model=RegisterResponse,+ status_code=status.HTTP_200_OK,+ summary="Register new telemetry client",+)+async def register():+ """+ Register a new Telemetry client.++ Returns a JWT token that can ONLY be used for Telemetry endpoints.+ The token includes an issuer claim that prevents cross-module usage.++ Returns:+ RegisterResponse: Client ID, JWT token, expiration+ """+ auth = get_telemetry_auth()+ return await auth.register_client()++[email protected](+ "/events",+ response_model=TelemetryBatchResponse,+ status_code=status.HTTP_200_OK,+ responses={+ 401: {"model": ErrorResponse, "description": "Authentication required"},+ 400: {"model": ErrorResponse, "description": "Invalid events"},+ },+ summary="Submit telemetry events",+)+async def submit_events(+ request: TelemetryBatchRequest,+ db: AsyncSession = Depends(get_db),+ client_id: str = Depends(get_current_client),+):+ """+ Submit telemetry events.@@ -10,7 +10,8 @@ import logging -from fastapi import APIRouter, Depends, Query, Request, Security, status+from fastapi import APIRouter, Body, Depends, Query, Request, Security, status+from pydantic import BaseModel from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy.ext.asyncio import AsyncSession from slowapi import Limiter@@ -76,6 +77,12 @@ async def register(request: Request): return await auth.register_client() +class RefreshRequest(BaseModel):+ """Request body for token refresh."""++ refresh_token: str++ @router.post( "/refresh", response_model=RegisterResponse,@@ -88,31 +95,31 @@ async def register(request: Request): @limiter.limit("60/hour") async def refresh_token( request: Request,- refresh_token: str = Query(..., description="Refresh token")+ body: RefreshRequest = Body(...), ): """ Use refresh token to get new access and refresh tokens (sliding expiration). SECURITY:- - Requires valid refresh token (7-day expiry)+ - Requires valid refresh token (7-day expiry) in POST body (not query params) - Returns new token pair with extended expiration - Implements sliding expiration: tokens auto-extend on use within TTL Token Flow: 1. Client uses access token (1-hour expiry) for API calls- 2. Before access token expires, client uses refresh token+ 2. Before access token expires, client sends refresh token in POST body 3. Server returns NEW access token (1 hour) + NEW refresh token (7 days) 4. This provides sliding expiration - active clients never locked out Args: request: FastAPI request- refresh_token: Valid refresh token (from registration or previous refresh)+ body: RefreshRequest containing the refresh token Returns: RegisterResponse: New access and refresh tokens """ auth = get_telemetry_auth()- result = auth.refresh_access_token(refresh_token)+ result = auth.refresh_access_token(body.refresh_token) return RegisterResponse( client_id=result["client_id"],AI-assisted change a7d5c0d64ec62c77713cfa90479663a0447a494086724c14f0aff6259838996a · Fix bb8ca8c979d7b3a0bf4ca3a84f525527af9fcf143da5190850d56db3d507b419