security: set SESSION_COOKIE_SECURE + validate IP before nginx deny write

Two hardening fixes from a fresh security pass of the live code:

- SESSION_COOKIE_SECURE=True: the session cookie set only HTTPONLY +
  SAMESITE=Lax, so it could be sent over plaintext HTTP. App is served
  behind nginx TLS, so no functional downside.
- _nginx_ban: validate the IP with ipaddress.ip_address() before
  interpolating it into the nginx deny file. A malformed value could
  otherwise inject nginx directives at the last write before reload.
  Guard sits at the sink, covering all current and future callers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-06-20 18:00:50 -05:00
parent 083f05c093
commit ea0ba33212

View File

@ -35,6 +35,7 @@ if not _flask_secret:
app.secret_key = _flask_secret
app.config["SESSION_COOKIE_HTTPONLY"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["SESSION_COOKIE_SECURE"] = True # served behind nginx TLS; prevents cookie leak over plaintext HTTP
# ─── SECURITY LOGGING ─────────────────────────────────────────
# Dedicated security log for fail2ban and audit trail.
@ -7687,6 +7688,16 @@ def _nginx_ban(ip):
if is_allowlisted(ip):
sec_log.info("NGINX_BAN_BLOCKED ip=%s — allowlisted, refused to write deny rule", ip)
return
# Defense in depth: validate IP format before interpolating into the
# nginx conf. A malformed value (e.g. "1.2.3.4;\n}\nserver{...") would
# otherwise inject directives into the deny file. Last write before
# reload — last place to stop a bad ban (matches docstring intent).
import ipaddress
try:
ipaddress.ip_address(ip)
except ValueError:
sec_log.warning("NGINX_BAN_INVALID_IP ip=%r — refused to write deny rule (possible nginx config injection)", ip)
return
import subprocess
line = f"deny {ip};\n"
# Each step has its own try/except so we know WHICH step failed.