From 0d8c94bf82ed1063f210d91d94a7d82aa7bbed92 Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Wed, 1 Apr 2026 19:44:15 -0400 Subject: [PATCH] Fix 6 security issues from post-FR audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix 3 missing CSRF tokens on admin user delete/reset and account key delete forms (were broken — CSRFProtect rejected submissions) - Fix trust store path traversal: untrust_key() now validates fingerprint format ([0-9a-f]{32}) and checks resolved path - Fix chain key rotation: old key is now revoked after rotation record, preventing compromised old keys from appending records - Fix SSRF in deadman webhook: block private/internal IP targets - Fix logout CSRF: /logout is now POST-only with CSRF token, preventing cross-site forced logout via img tags Co-Authored-By: Claude Opus 4.6 (1M context) --- frontends/web/app.py | 2 +- frontends/web/templates/account.html | 1 + frontends/web/templates/admin/users.html | 2 ++ frontends/web/templates/base.html | 2 +- src/soosef/federation/chain.py | 2 ++ src/soosef/fieldkit/deadman.py | 24 +++++++++++++++++------- src/soosef/keystore/manager.py | 5 +++++ 7 files changed, 29 insertions(+), 9 deletions(-) diff --git a/frontends/web/app.py b/frontends/web/app.py index dce911f..44f6fed 100644 --- a/frontends/web/app.py +++ b/frontends/web/app.py @@ -374,7 +374,7 @@ def _register_stegasoo_routes(app: Flask) -> None: flash("Invalid username or password", "error") return render_template("login.html") - @app.route("/logout") + @app.route("/logout", methods=["POST"]) def logout(): auth_logout_user() flash("Logged out successfully", "success") diff --git a/frontends/web/templates/account.html b/frontends/web/templates/account.html index a179f8b..c891a66 100644 --- a/frontends/web/templates/account.html +++ b/frontends/web/templates/account.html @@ -157,6 +157,7 @@
+ diff --git a/frontends/web/templates/admin/users.html b/frontends/web/templates/admin/users.html index d3c520c..bc90d30 100644 --- a/frontends/web/templates/admin/users.html +++ b/frontends/web/templates/admin/users.html @@ -65,12 +65,14 @@ {% if user.id != current_user.id %} +
+ diff --git a/frontends/web/templates/base.html b/frontends/web/templates/base.html index 1b26b6b..48dd849 100644 --- a/frontends/web/templates/base.html +++ b/frontends/web/templates/base.html @@ -102,7 +102,7 @@ {% endif %}
  • Keys
  • -
  • Logout
  • +
  • {% else %} diff --git a/src/soosef/federation/chain.py b/src/soosef/federation/chain.py index 2b6b97d..4b697f2 100644 --- a/src/soosef/federation/chain.py +++ b/src/soosef/federation/chain.py @@ -499,6 +499,8 @@ class ChainStore: f"Record {record.chain_index}: key rotation missing new_pubkey" ) authorized_signers.add(bytes.fromhex(new_pubkey_hex)) + # Revoke the old key — the rotation record was its last authorized action + authorized_signers.discard(record.signer_pubkey) prev_record = record expected_index += 1 diff --git a/src/soosef/fieldkit/deadman.py b/src/soosef/fieldkit/deadman.py index 46ead8b..e269bce 100644 --- a/src/soosef/fieldkit/deadman.py +++ b/src/soosef/fieldkit/deadman.py @@ -114,14 +114,24 @@ class DeadmanSwitch: if webhook: try: - import urllib.request + from urllib.parse import urlparse - data = json.dumps({"text": message}).encode() - req = urllib.request.Request( - webhook, data=data, headers={"Content-Type": "application/json"} - ) - urllib.request.urlopen(req, timeout=10) - logger.info("Deadman warning sent to webhook") + parsed = urlparse(webhook) + hostname = parsed.hostname or "" + # Block private/internal targets to prevent SSRF + blocked = hostname in ("localhost", "127.0.0.1", "::1", "0.0.0.0") + blocked = blocked or hostname.startswith(("10.", "172.16.", "192.168.", "169.254.")) + if blocked: + logger.warning("Webhook blocked: internal/private URL %s", hostname) + else: + import urllib.request + + data = json.dumps({"text": message}).encode() + req = urllib.request.Request( + webhook, data=data, headers={"Content-Type": "application/json"} + ) + urllib.request.urlopen(req, timeout=10) + logger.info("Deadman warning sent to webhook") except Exception as e: logger.error("Failed to send deadman webhook: %s", e) diff --git a/src/soosef/keystore/manager.py b/src/soosef/keystore/manager.py index 4ec2464..fdfc5fe 100644 --- a/src/soosef/keystore/manager.py +++ b/src/soosef/keystore/manager.py @@ -380,9 +380,14 @@ class KeystoreManager: def untrust_key(self, fingerprint: str) -> bool: """Remove a key from the trust store. Returns True if found and removed.""" + import re import shutil + if not re.fullmatch(r"[0-9a-f]{32}", fingerprint): + raise KeystoreError(f"Invalid fingerprint format: {fingerprint!r}") key_dir = self._trusted_keys_dir / fingerprint + if not key_dir.resolve().is_relative_to(self._trusted_keys_dir.resolve()): + raise KeystoreError("Path traversal detected") if key_dir.exists(): shutil.rmtree(key_dir) return True