From fd9bb0a4364fdbecb86fc9f453c0aeb2fc6e610a Mon Sep 17 00:00:00 2001 From: v6ole Date: Tue, 4 Aug 2026 11:05:04 +0800 Subject: [PATCH] fix: fail closed on production settings --- backend/app/config.py | 3 ++- backend/app/services/settings_validation.py | 2 +- backend/tests/test_settings_validation.py | 30 +++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index 0f29a2a..4deafe4 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -2,6 +2,7 @@ from pydantic import Field from pydantic_settings import BaseSettings +from typing import Literal LEGACY_DEFAULT_SECRET = "change-me-to-a-long-random-string" @@ -9,7 +10,7 @@ LEGACY_DEFAULT_SECRET = "change-me-to-a-long-random-string" class Settings(BaseSettings): # ---------- 运行环境 ---------- - environment: str = "development" + environment: Literal["development", "test", "production"] = "development" # ---------- 数据库 ---------- DATABASE_URL: str = "sqlite+aiosqlite:///./pingwatch.db" diff --git a/backend/app/services/settings_validation.py b/backend/app/services/settings_validation.py index 43e6560..9f862f2 100644 --- a/backend/app/services/settings_validation.py +++ b/backend/app/services/settings_validation.py @@ -6,7 +6,7 @@ from app.config import LEGACY_DEFAULT_SECRET, Settings def validate_runtime_settings(settings: Settings) -> None: """Reject unsafe settings before scheduling monitoring work.""" if settings.environment == "production": - if settings.secret_key == LEGACY_DEFAULT_SECRET: + if not settings.secret_key.strip() or settings.secret_key == LEGACY_DEFAULT_SECRET: raise ValueError("SECRET_KEY must be provided by the runtime environment") if not settings.casdoor_endpoint.startswith("https://"): raise ValueError("CASDOOR endpoint must use TLS in production") diff --git a/backend/tests/test_settings_validation.py b/backend/tests/test_settings_validation.py index fefd1fa..48386d8 100644 --- a/backend/tests/test_settings_validation.py +++ b/backend/tests/test_settings_validation.py @@ -15,11 +15,41 @@ def test_production_rejects_legacy_jwt_secret(): validate_runtime_settings(settings) +@pytest.mark.parametrize("secret_key", ["", " ", "\t\n"]) +def test_production_rejects_blank_jwt_secret(secret_key): + settings = Settings(environment="production", secret_key=secret_key) + + with pytest.raises(ValueError, match="SECRET_KEY"): + validate_runtime_settings(settings) + + +@pytest.mark.parametrize("environment", ["prod", "Production", "staging"]) +def test_environment_must_be_an_explicit_supported_value(environment): + with pytest.raises(ValidationError, match="environment"): + Settings(environment=environment) + + def test_probe_packet_count_must_be_positive(): with pytest.raises(ValidationError): Settings(probe_packets_per_round=0) +@pytest.mark.parametrize( + ("field", "invalid_value"), + [ + ("offline_consecutive_rounds", 0), + ("degraded_window_rounds", 1), + ("degraded_loss_percent", 0.0), + ("recovery_consecutive_clean_rounds", 0), + ("wecom_notification_max_attempts", 0), + ("wecom_retry_base_seconds", 0), + ], +) +def test_monitoring_and_retry_settings_reject_values_below_their_bounds(field, invalid_value): + with pytest.raises(ValidationError): + Settings(**{field: invalid_value}) + + def test_production_rejects_tls_disabled_casdoor(): settings = Settings( environment="production",