Files
rengine/api/app/config.py
T
Yogesh OjhaandClaude Opus 5 5784a75ff7 fix(api): the global rate limit could be lifted with one forged header
GlobalRateLimitMiddleware keyed its per-client bucket on the first
entry of X-Forwarded-For whenever the header was present. Nothing in
the shipped stack writes that header — the Vite proxy does not set
xfwd — so in practice the value was always supplied by the caller, and
rotating it per request gave every request a fresh bucket. Measured:
40 requests with a rotating header all returned 200 while 20 from the
same client without one were refused.

Reading the leftmost entry is also wrong behind a real reverse proxy,
which appends the peer address rather than replacing the header, so a
caller who prepends a value of their own is keyed on that value even in
a correct deployment.

client_id() now walks the chain right to left and returns the first hop
that is not itself a trusted proxy, and only consults the header at all
when the immediate peer is trusted. TRUSTED_PROXIES (empty by default)
names those hops as IPs or CIDRs; with no value set the peer address is
the only identity considered. The 429 now carries Retry-After.

Login is keyed by username and was never affected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6LcH8uUddjJbKrahf34ys
2026-09-07 06:47:41 +05:30

58 lines
1.6 KiB
Python

from functools import lru_cache
from pydantic import field_validator
from shared.config import BaseAppSettings
class Settings(BaseAppSettings):
API_V1_PREFIX: str = "/api/v1"
CORS_ORIGINS: list[str] = ["http://localhost:5173"] # noqa: RUF012
SECRET_KEY: str = "change-me-in-production-use-openssl-rand-hex-32" # noqa: S105
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
GLOBAL_RATE_LIMIT_PER_MINUTE: int = 600
TRUSTED_PROXIES: str = ""
ADMIN_EMAIL: str = "admin@rengine.local"
ADMIN_USERNAME: str = "rengine"
ADMIN_PASSWORD: str = "rengine@123" # noqa: S105
@property
def database_url(self) -> str:
return self.database_url_async
@field_validator("SECRET_KEY")
@classmethod
def validate_secret_key(cls, v: str, info) -> str:
if (
not info.data.get("DEBUG", False)
and v == "change-me-in-production-use-openssl-rand-hex-32"
):
msg = (
"SECRET_KEY must be set in production. "
"Generate one with: openssl rand -hex 32"
)
raise ValueError(msg)
return v
@field_validator("ADMIN_PASSWORD")
@classmethod
def validate_admin_password(cls, v: str, info) -> str:
if not info.data.get("DEBUG", False) and v == "rengine@123":
msg = "ADMIN_PASSWORD must be changed from default value in production"
raise ValueError(msg)
return v
@lru_cache
def get_settings() -> Settings:
return Settings()
settings = get_settings()