application.yaml
CORS Configuration
Section titled “CORS Configuration”CORSConfig controls cross-origin resource sharing. Never use * in production with credentials.
web: cors: enabled: true allowed_origins: - "https://app.myapp.com" - "https://admin.myapp.com" allow_credentials: true allow_methods: - GET - POST - PUT - DELETE - PATCH expose_headers: - X-Request-Id max_age: 600The CORSConfig validator rejects allow_credentials=True combined with allowed_origins=['*'] — browsers block such responses.
# ✅ Correct — explicit origins with credentialsconfig = CORSConfig(allowed_origins=["https://myapp.com"], allow_credentials=True)
# ❌ RuntimeError — browser-incompatibleconfig = CORSConfig(allowed_origins=["*"], allow_credentials=True)In production, WebConfig.validate_production_security() blocks wildcard CORS at boot:
# Raises ValueError at boot:web: cors: allowed_origins: ["*"]CSRF Protection
Section titled “CSRF Protection”CSRF protection is enabled by default via lexigram-web’s SecurityConfig:
web: security: csrf: enabled: true cookie_name: csrf_token header_name: X-CSRF-Token cookie_secure: true # HTTPS only cookie_httponly: true cookie_samesite: Lax token_length: 32 token_ttl: 3600 # 1 hour excluded_paths: - /api/ - /health - /metricsThe CSRFProtectionMiddleware sets a signed cookie on GET requests and validates the X-CSRF-Token header on state-changing methods (POST, PUT, DELETE, PATCH).
from lexigram.web.security.csrf.protection import CSRFProtection
# Already registered by WebProvider.boot() — resolve from containercsrf: CSRFProtection = await container.resolve(CSRFProtection)token = await csrf.generate_token()# Set as X-CSRF-Token header on the clientRate Limiting
Section titled “Rate Limiting”RateLimitConfig supports per-path rules with sliding window (Redis-backed) or fixed-window (cache-backed) algorithms:
web: rate_limit: enabled: true default_limit: 100 default_window: 60 storage_backend: redis # or "memory" (dev only) rules: "/api/auth/login": requests: 10 window: 60 burst: 5 "/api/webhook": requests: 200 window: 60The RateLimitMiddleware operates per-IP by default. Use the RateLimiter directly for per-user limits:
from lexigram.web.middleware.rate_limit import RateLimiter
limiter = RateLimiter(window=60, max_requests=30)if await limiter.try_acquire(client_id="user:42"): # proceed passInput Sanitization
Section titled “Input Sanitization”InputSanitizationMiddleware strips null bytes and rejects obvious script-injection patterns from query parameters before they reach controllers:
from lexigram.web.middleware.sanitization import InputSanitizationMiddleware
# Added automatically when WebProvider is configured# Sanitizes: null bytes, <script> tags, javascript: URIsThis is defense-in-depth — validate at the service layer too. The middleware does not inspect request bodies.
Security Headers
Section titled “Security Headers”SecurityHeadersMiddleware emits standard security headers with sensible defaults:
web: security: hsts: enabled: true max_age: 31536000 # 1 year include_subdomains: true preload: false csp: enabled: true directives: default-src: "'self'" script-src: "'self'" style-src: "'self' 'unsafe-inline'" img-src: "'self' data: https: blob:" frame-ancestors: "'none'" base-uri: "'self'" form-action: "'self'" cross_origin: enabled: true opener_policy: same-origin embedder_policy: require-corpUse create_production_config() for secure defaults or create_development_config() for local dev:
from lexigram.web.middleware.security import ( create_production_config, create_development_config,)
production = create_production_config() # strict HSTS, full CSPdevelopment = create_development_config() # relaxed CSP, no HSTSSession Management
Section titled “Session Management”SessionCookieBackend stores session data in signed cookies (default) or server-side via a SessionRepositoryProtocol backend:
web: security: session: cookie_name: session cookie_secure: true cookie_httponly: true cookie_samesite: Lax max_age: 86400 # 24 hoursError Response Hardening
Section titled “Error Response Hardening”lexigram-web never leaks internal tracebacks in production. The DefaultExceptionFilter returns sanitized error bodies:
from lexigram.web.filters import DefaultExceptionFilter
# Debug mode includes stack traces; production strips themfilter = DefaultExceptionFilter(debug=False)Custom error bodies use ProblemDetail (RFC 9457):
from lexigram.web.errors.problem_detail import ProblemDetail
class MyError(HTTPError): def problem_detail(self) -> ProblemDetail: return ProblemDetail( type="https://docs.myapp.com/errors/rate-limit", title="Rate Limit Exceeded", status=429, detail="Too many requests. Retry after 30 seconds.", )