Files
aurum/backend/app/core/config.py
technosexual b045d378f0 fix(security): stop granting every origin credentialed access to the API (#5)
`AURUM_CORS_ORIGINS` defaults to `*`, and the middleware was configured with
`allow_credentials=True`. Starlette does not answer that combination with a
literal `*` — it echoes back whatever `Origin` the caller sent and adds
`Access-Control-Allow-Credentials: true`, so any page open in the same
browser can call the instance, with the user's Basic Auth attached, and read
the response:

    GET /api/accounts   Origin: https://evil.example
    -> Access-Control-Allow-Origin: https://evil.example
       Access-Control-Allow-Credentials: true

That undercuts the README's advice that leaving Basic Auth off is fine while
the instance is only reachable from localhost: reachable-from-localhost
includes every website the user visits.

Nothing in the shipped setup is cross-origin. Compose serves the UI and the
API from the same nginx, and the Vite dev server proxies `/api`
(frontend/vite.config.ts), so neither deployment nor development needs CORS
at all. The default is therefore empty — no cross-origin access — and the
middleware is only installed when origins are configured deliberately.

An explicit `*` is still honoured for a genuinely public read-only setup, but
never together with credentials, which is what the spec intends `*` to mean
anyway.
2026-08-30 18:11:09 +03:00

54 lines
2.0 KiB
Python

"""Application configuration, sourced from environment variables (.env)."""
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
# Single source of truth for the running app's version — surfaced in the API
# title and embedded in exported backups so an old file can be told apart
# from a current one.
APP_VERSION = "0.20.0"
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_prefix="AURUM_", extra="ignore")
# Postgres connection
postgres_user: str = "aurum"
postgres_password: str = "aurum"
postgres_db: str = "aurum"
postgres_host: str = "db"
postgres_port: int = 5432
# Default currency shown across the UI when an account doesn't override it
default_currency: str = "USD"
# Comma-separated list of browser origins allowed to call the API. Empty
# by default, which allows none: the shipped compose serves the UI and the
# API from one nginx, and the Vite dev server proxies /api, so neither is
# a cross-origin caller. Set it only for a genuinely separate frontend.
cors_origins: str = ""
# CoinGecko Demo API key (free, no card required — https://www.coingecko.com/en/api/pricing)
# for services/crypto_service.py's price lookups. Empty by default; the
# Crypto tab's endpoints 400 with a clear message until this is set,
# rather than silently hitting CoinGecko's much stingier keyless tier.
coingecko_api_key: str = ""
@property
def database_url(self) -> str:
return (
f"postgresql+asyncpg://{self.postgres_user}:{self.postgres_password}"
f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"
)
@property
def cors_origins_list(self) -> list[str]:
if self.cors_origins == "*":
return ["*"]
return [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()]
@lru_cache
def get_settings() -> Settings:
return Settings()