Author SHA1 Message Date
profit b8726d03b6 Merge branch 'main' into fix/code-review-mode-alias 2026-06-21 04:00:10 +00:00
profit 86b30939fa Merge pull request 'security: SESSION_COOKIE_SECURE + nginx IP validation' (#3) from security/session-cookie-and-nginx-ip-guard into main 2026-06-21 03:57:23 +00:00
rootandClaude Opus 4.8 5625df924f fix(modes): accept code_review as alias for codereview
External callers (lakehouse observer.ts) escalate to /api/run with mode
"code_review" (underscored), but the canonical registered mode is "codereview",
so those calls hit the unknown-mode 400 and silently dropped. Normalize the alias
right after reading the mode so it validates and dispatches via RUNNERS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:47:07 -05:00
rootandClaude Opus 4.8 ea0ba33212 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>
2026-06-20 18:00:50 -05:00
+16
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.
@@ -10769,6 +10780,11 @@ def run_team():
return jsonify({"error": "Request body required"}), 400
mode = config.get("mode", "")
# Alias: external callers (e.g. lakehouse observer.ts) escalate to the underscored
# `code_review`, but the canonical registered mode is `codereview` — normalize so
# those calls dispatch via RUNNERS instead of silently failing the 400 below.
if mode == "code_review":
mode = "codereview"
if not mode:
return jsonify({"error": "Mode is required"}), 400