forked from username/flaskpaste
security: implement quick win remediations (FLOOD-001, CLI-002, CLI-003, AUDIT-001)
FLOOD-001: Cap anti-flood request list at configurable max entries - Add ANTIFLOOD_MAX_ENTRIES config (default 10000) - Prune oldest entries when limit exceeded CLI-002: Explicitly set SSL hostname verification - Add ctx.check_hostname = True and ctx.verify_mode = CERT_REQUIRED - Defense in depth (create_default_context sets these by default) CLI-003: Warn on insecure config file permissions - Check if config file is world-readable - Print warning to stderr if permissions too open AUDIT-001: Already implemented - query has LIMIT/OFFSET with 500 max
This commit is contained in:
27
fpaste
27
fpaste
@@ -10,6 +10,7 @@ import json
|
||||
import os
|
||||
import shutil
|
||||
import ssl
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
@@ -119,6 +120,11 @@ def die(msg: str, code: int = 1) -> NoReturn:
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
def warn(msg: str) -> None:
|
||||
"""Print warning to stderr."""
|
||||
print(f"warning: {msg}", file=sys.stderr)
|
||||
|
||||
|
||||
def request(
|
||||
url: str,
|
||||
method: str = "GET",
|
||||
@@ -152,6 +158,20 @@ def parse_error(body: bytes, default: str = "request failed") -> str:
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def check_config_permissions(path: Path) -> None:
|
||||
"""CLI-003: Warn if config file has insecure permissions."""
|
||||
try:
|
||||
mode = path.stat().st_mode
|
||||
# Warn if group or others can read (should be 600 or 640)
|
||||
if mode & stat.S_IROTH:
|
||||
warn(f"config file {path} is world-readable (mode {stat.filemode(mode)})")
|
||||
elif mode & stat.S_IRGRP:
|
||||
# Group-readable is less severe, only warn if also has secrets
|
||||
pass # Silent for group-readable, common in shared setups
|
||||
except OSError:
|
||||
pass # File may not exist yet or permission denied
|
||||
|
||||
|
||||
def read_config_file(path: Path | None = None) -> dict[str, str]:
|
||||
"""Read config file and return key-value pairs."""
|
||||
path = path or CONFIG_FILE
|
||||
@@ -160,6 +180,9 @@ def read_config_file(path: Path | None = None) -> dict[str, str]:
|
||||
if not path.exists():
|
||||
return result
|
||||
|
||||
# CLI-003: Check file permissions before reading
|
||||
check_config_permissions(path)
|
||||
|
||||
for line in path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
@@ -214,6 +237,10 @@ def create_ssl_context(config: Mapping[str, Any]) -> ssl.SSLContext | None:
|
||||
return None
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
# CLI-002: Explicitly enable hostname verification (defense in depth)
|
||||
# create_default_context() sets these, but explicit is safer
|
||||
ctx.check_hostname = True
|
||||
ctx.verify_mode = ssl.CERT_REQUIRED
|
||||
|
||||
if ca_cert := config.get("ca_cert", ""):
|
||||
ctx.load_verify_locations(ca_cert)
|
||||
|
||||
Reference in New Issue
Block a user