Aug 26 - Enhance security 2
CI / Python lint (flake8) (push) Has been cancelled
CI / Python syntax check (push) Has been cancelled
CI / Alembic migration chain (push) Has been cancelled
CI / JavaScript syntax check (push) Has been cancelled
CI / Pytest (push) Has been cancelled
CI / Build extension zip (push) Has been cancelled
CI / Python lint (flake8) (push) Has been cancelled
CI / Python syntax check (push) Has been cancelled
CI / Alembic migration chain (push) Has been cancelled
CI / JavaScript syntax check (push) Has been cancelled
CI / Pytest (push) Has been cancelled
CI / Build extension zip (push) Has been cancelled
This commit is contained in:
+70
-28
@@ -78,6 +78,11 @@ class BaseConfig:
|
||||
# Set via .env: STATIC_VERSION=20260418
|
||||
STATIC_VERSION = os.environ.get('STATIC_VERSION', '1')
|
||||
|
||||
# Background cleanup scheduler (token_blacklist / recovery_challenges /
|
||||
# totp_used_codes / expired shares). Disabled under test so the suite does
|
||||
# not spawn a daemon thread per app fixture.
|
||||
SCHEDULER_ENABLED = True
|
||||
|
||||
# Session cookie defaults — applied in all environments.
|
||||
# SECURE is intentionally left out of BaseConfig so dev HTTP still works.
|
||||
# See ProductionConfig below for the full hardened set.
|
||||
@@ -90,6 +95,35 @@ class DevelopmentConfig(BaseConfig):
|
||||
RATELIMIT_ENABLED = False
|
||||
|
||||
|
||||
class TestingConfig(BaseConfig):
|
||||
"""
|
||||
In-memory SQLite, no rate limiting, no background threads.
|
||||
|
||||
SQLite is viable here because the only MySQL-specific construct in the
|
||||
models is mysql.INTEGER(unsigned=True), which SQLAlchemy renders as a plain
|
||||
INTEGER on other dialects. That means these tests cover application logic
|
||||
and flow, NOT MySQL-specific behaviour (collation, ON UPDATE NOW(), unsigned
|
||||
range) — schema changes still need a real migration run against MySQL.
|
||||
"""
|
||||
TESTING = True
|
||||
DEBUG = False
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
|
||||
SQLALCHEMY_ENGINE_OPTIONS = {}
|
||||
RATELIMIT_ENABLED = False
|
||||
SCHEDULER_ENABLED = False
|
||||
WTF_CSRF_ENABLED = False
|
||||
SECRET_KEY = 'test-secret-not-used-in-production'
|
||||
JWT_SECRET_KEY = 'test-jwt-secret-not-used-in-production'
|
||||
TOTP_ENCRYPTION_KEY = '00' * 32
|
||||
CORS_ORIGINS = 'http://localhost'
|
||||
WEBAUTHN_RP_ID = 'localhost'
|
||||
WEBAUTHN_ORIGINS = ['http://localhost']
|
||||
# Keep Argon2 cheap so the suite is not dominated by password hashing.
|
||||
ARGON2_TIME_COST = 1
|
||||
ARGON2_MEMORY_COST = 8
|
||||
ARGON2_PARALLELISM = 1
|
||||
|
||||
|
||||
class ProductionConfig(BaseConfig):
|
||||
DEBUG = False
|
||||
RATELIMIT_ENABLED = True
|
||||
@@ -98,40 +132,48 @@ class ProductionConfig(BaseConfig):
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
|
||||
# ── Critical security checks — fail loudly at startup, not silently at runtime ──
|
||||
# These checks run at class definition time (i.e. at import / app startup).
|
||||
# Any misconfiguration raises RuntimeError immediately so the process never
|
||||
# serves a single request with an insecure configuration.
|
||||
@classmethod
|
||||
def validate(cls):
|
||||
"""
|
||||
Fail loudly on insecure production configuration.
|
||||
|
||||
_secret_key = os.environ.get('SECRET_KEY', '')
|
||||
if not _secret_key or _secret_key in _INSECURE_SECRET_DEFAULTS:
|
||||
raise RuntimeError(
|
||||
'[PassKeeper] SECRET_KEY is not set or uses an insecure placeholder. '
|
||||
'Generate a strong key with: python -c "import secrets; print(secrets.token_hex(32))" '
|
||||
'and add SECRET_KEY=<value> to your production .env file.'
|
||||
)
|
||||
SECRET_KEY = _secret_key
|
||||
Called from create_app() when this config is selected — NOT at class
|
||||
definition time. Running it in the class body meant merely *importing*
|
||||
app.config raised unless production secrets were present in the
|
||||
environment, which broke the test suite and any local tooling that
|
||||
imports the app (including `flask db upgrade` on a dev box).
|
||||
|
||||
_jwt_secret = os.environ.get('JWT_SECRET_KEY', '')
|
||||
if not _jwt_secret or _jwt_secret in _INSECURE_SECRET_DEFAULTS:
|
||||
raise RuntimeError(
|
||||
'[PassKeeper] JWT_SECRET_KEY is not set or uses an insecure placeholder. '
|
||||
'Generate a strong key with: python -c "import secrets; print(secrets.token_hex(32))" '
|
||||
'and add JWT_SECRET_KEY=<value> to your production .env file.'
|
||||
)
|
||||
JWT_SECRET_KEY = _jwt_secret
|
||||
The fail-loud property is preserved: create_app('production') raises
|
||||
before the app is returned, so the process still never serves a request
|
||||
under an insecure configuration.
|
||||
"""
|
||||
secret_key = os.environ.get('SECRET_KEY', '')
|
||||
if not secret_key or secret_key in _INSECURE_SECRET_DEFAULTS:
|
||||
raise RuntimeError(
|
||||
'[PassKeeper] SECRET_KEY is not set or uses an insecure placeholder. '
|
||||
'Generate a strong key with: python -c "import secrets; print(secrets.token_hex(32))" '
|
||||
'and add SECRET_KEY=<value> to your production .env file.'
|
||||
)
|
||||
|
||||
_cors = os.environ.get('CORS_ORIGINS', '')
|
||||
if not _cors or _cors.strip() == '*':
|
||||
raise RuntimeError(
|
||||
'[PassKeeper] CORS_ORIGINS must be set to a specific origin in production '
|
||||
'(e.g. CORS_ORIGINS=https://pwkeeper.ngodanguyen.tech). '
|
||||
'A wildcard "*" is not permitted in production.'
|
||||
)
|
||||
CORS_ORIGINS = _cors
|
||||
jwt_secret = os.environ.get('JWT_SECRET_KEY', '')
|
||||
if not jwt_secret or jwt_secret in _INSECURE_SECRET_DEFAULTS:
|
||||
raise RuntimeError(
|
||||
'[PassKeeper] JWT_SECRET_KEY is not set or uses an insecure placeholder. '
|
||||
'Generate a strong key with: python -c "import secrets; print(secrets.token_hex(32))" '
|
||||
'and add JWT_SECRET_KEY=<value> to your production .env file.'
|
||||
)
|
||||
|
||||
cors = os.environ.get('CORS_ORIGINS', '')
|
||||
if not cors or cors.strip() == '*':
|
||||
raise RuntimeError(
|
||||
'[PassKeeper] CORS_ORIGINS must be set to a specific origin in production '
|
||||
'(e.g. CORS_ORIGINS=https://pwkeeper.ngodanguyen.tech). '
|
||||
'A wildcard "*" is not permitted in production.'
|
||||
)
|
||||
|
||||
|
||||
config = {
|
||||
'development': DevelopmentConfig,
|
||||
'testing': TestingConfig,
|
||||
'production': ProductionConfig,
|
||||
}
|
||||
Reference in New Issue
Block a user