flaskpaste: initial commit with security hardening

Features:
- REST API for text/binary pastes with MIME detection
- Client certificate auth via X-SSL-Client-SHA1 header
- SQLite with WAL mode for concurrent access
- Automatic paste expiry with LRU cleanup

Security:
- HSTS, CSP, X-Frame-Options, X-Content-Type-Options
- Cache-Control: no-store for sensitive responses
- X-Request-ID tracing for log correlation
- X-Proxy-Secret validation for defense-in-depth
- Parameterized queries, input validation
- Size limits (3 MiB anon, 50 MiB auth)

Includes /health endpoint, container support, and 70 tests.
This commit is contained in:
Username
2025-12-16 04:42:18 +01:00
commit 8f9868f0d9
21 changed files with 2588 additions and 0 deletions

56
app/config.py Normal file
View File

@@ -0,0 +1,56 @@
"""Application configuration."""
import os
from pathlib import Path
class Config:
"""Base configuration."""
BASE_DIR = Path(__file__).parent.parent
DATABASE = os.environ.get("FLASKPASTE_DB", BASE_DIR / "data" / "pastes.db")
PASTE_ID_LENGTH = int(os.environ.get("FLASKPASTE_ID_LENGTH", "12"))
# Paste size limits
MAX_PASTE_SIZE_ANON = int(os.environ.get("FLASKPASTE_MAX_ANON", 3 * 1024 * 1024)) # 3MiB
MAX_PASTE_SIZE_AUTH = int(os.environ.get("FLASKPASTE_MAX_AUTH", 50 * 1024 * 1024)) # 50MiB
MAX_CONTENT_LENGTH = MAX_PASTE_SIZE_AUTH # Flask request limit
# Paste expiry (default 5 days)
PASTE_EXPIRY_SECONDS = int(os.environ.get("FLASKPASTE_EXPIRY", 5 * 24 * 60 * 60))
# Reverse proxy trust configuration
# SECURITY: The X-SSL-Client-SHA1 header is trusted for authentication.
# This header MUST only come from a trusted reverse proxy that validates
# client certificates. Direct access to this app MUST be blocked.
#
# Set FLASKPASTE_PROXY_SECRET to require the proxy to send a matching
# X-Proxy-Secret header, providing defense-in-depth against header spoofing.
TRUSTED_PROXY_SECRET = os.environ.get("FLASKPASTE_PROXY_SECRET", "")
class DevelopmentConfig(Config):
"""Development configuration."""
DEBUG = True
class ProductionConfig(Config):
"""Production configuration."""
DEBUG = False
class TestingConfig(Config):
"""Testing configuration."""
TESTING = True
DATABASE = ":memory:"
config = {
"development": DevelopmentConfig,
"production": ProductionConfig,
"testing": TestingConfig,
"default": DevelopmentConfig,
}