diff --git a/src/config.py b/src/config.py index 3750e88a..77f402d9 100644 --- a/src/config.py +++ b/src/config.py @@ -469,8 +469,19 @@ def validate_startup(*, force: bool = False) -> None: ", ".join(_missing), ) sys.exit(1) + if "*" in settings.cors_origins: + _startup_logger.error( + "PRODUCTION SECURITY ERROR: CORS wildcard '*' is not allowed " + "in production. Set CORS_ORIGINS to explicit origins." + ) + sys.exit(1) _startup_logger.info("Production mode: security secrets validated ✓") else: + if "*" in settings.cors_origins: + _startup_logger.warning( + "SEC: CORS_ORIGINS contains wildcard '*' — " + "restrict to explicit origins before deploying to production." + ) if not settings.l402_hmac_secret: _startup_logger.warning( "SEC: L402_HMAC_SECRET is not set — " diff --git a/tests/test_lazy_init.py b/tests/test_lazy_init.py index 3e881733..9d63665b 100644 --- a/tests/test_lazy_init.py +++ b/tests/test_lazy_init.py @@ -49,6 +49,34 @@ class TestConfigLazyValidation: # Should not raise validate_startup(force=True) + def test_validate_startup_exits_on_cors_wildcard_in_production(self): + """validate_startup() should exit in production when CORS has wildcard.""" + from config import settings, validate_startup + + with ( + patch.object(settings, "timmy_env", "production"), + patch.object(settings, "l402_hmac_secret", "test-secret-hex-value-32"), + patch.object(settings, "l402_macaroon_secret", "test-macaroon-hex-value-32"), + patch.object(settings, "cors_origins", ["*"]), + pytest.raises(SystemExit), + ): + validate_startup(force=True) + + def test_validate_startup_warns_cors_wildcard_in_dev(self): + """validate_startup() should warn in dev when CORS has wildcard.""" + from config import settings, validate_startup + + with ( + patch.object(settings, "timmy_env", "development"), + patch.object(settings, "cors_origins", ["*"]), + patch("config._startup_logger") as mock_logger, + ): + validate_startup(force=True) + mock_logger.warning.assert_any_call( + "SEC: CORS_ORIGINS contains wildcard '*' — " + "restrict to explicit origins before deploying to production." + ) + def test_validate_startup_skips_in_test_mode(self): """validate_startup() should be a no-op in test mode.""" from config import validate_startup