Skip to content

[critical] fix: [website] register state-changing routes as POST, not GET - #878

Open
elhoim wants to merge 1 commit into
MISP:mainfrom
elhoim:fix/069-csrf-get-state-routes
Open

[critical] fix: [website] register state-changing routes as POST, not GET#878
elhoim wants to merge 1 commit into
MISP:mainfrom
elhoim:fix/069-csrf-get-state-routes

Conversation

@elhoim

@elhoim elhoim commented Aug 31, 2026

Copy link
Copy Markdown
Member

BLUF — Three state-changing website routes were GET and thus unprotected by CSRF; they are now POST.

  • Problem/change_status in website/app/home.py and /history/remove_node_session and /history/remove_node_tree in website/app/history/history.py are registered as GET, and Flask-WTF's CSRFProtect only validates unsafe methods, so they carry no CSRF protection at all.
  • Fix — Re-register the three routes as POST and update their callers in modules_config.html, history_session.html and history_view.js to send the X-CSRFToken header.
  • Effect — An admin who visits a hostile page can no longer have a module silently toggled or a history node deleted by a cross-site request; parameters, handler logic and responses are unchanged.

Three state-changing Flask routes were registered as GET, but Flask-WTF's CSRFProtect only validates "unsafe" HTTP methods (POST/PUT/PATCH/DELETE) by default and leaves GET requests unchecked:

@home_blueprint.route("/change_status", methods=["GET"])
def change_status():
    ...

@history_blueprint.route("/history/remove_node_session/<sid>", methods=["GET"])
def remove_node_session(sid):
    ...

@history_blueprint.route("/history/remove_node_tree/<sid>", methods=["GET"])
def remove_node_tree(sid):
    ...

Because these are simple GET requests, they are trivially triggerable cross-site — e.g. via <img src="https://target/change_status?module_id=..."> embedded on an attacker-controlled page — bypassing CSRF protection entirely.

Impact: A logged-in MISP-modules website admin who visits a malicious page (or one with attacker-controlled content) can have a module silently disabled/enabled, or a history session/tree node silently deleted, without any interaction or confirmation, and without the CSRF token check that protects the equivalent POST routes (e.g. /change_config).

Fix: Changed the three routes to methods=["POST"] so CSRFProtect actually covers them, matching the existing pattern used by /change_config. Updated the corresponding JS/template callers (modules_config.html, history_session.html, history_view.js) to issue POST requests with the X-CSRFToken header instead of plain GET fetches.

This is a behaviour change only in HTTP method (GET → POST) for these three endpoints; the request parameters, handler logic, and responses are unchanged, and all callers in the codebase were updated to match.

Verification

The website/ app has no automated test coverage in this repository. Verification was:

  • py_compile clean on website/app/home.py and website/app/history/history.py
  • JS/HTML template edits re-read carefully for correctness
  • Full module test suite still passing: 161 passed, 4 skipped, 5 subtests passed in 74.97s (0:01:14) — matches baseline

Found during a review of the repository; other findings are being submitted as separate PRs.

🤖 Generated with Claude Code

change_status (home.py) and the history remove_node_session/remove_node_tree
routes were registered with methods=["GET"]. Flask-WTF's CSRFProtect only
validates unsafe HTTP methods (POST/PUT/PATCH/DELETE), so a GET route that
mutates state bypasses CSRF protection entirely: a module can be silently
disabled, or a history node deleted, just by getting a logged-in admin's
browser to load an attacker-controlled <img src="/change_status?module_id=..">
or similar cross-site request, with no confirmation and no CSRF token check.

Switched the three routes to methods=["POST"] so CSRFProtect covers them,
and updated their JS/template callers (modules_config.html, history_session.html,
history_view.js) to issue POST requests carrying the X-CSRFToken header,
matching the pattern already used by /change_config.

Verified with py_compile on the two changed Python files (flake8 excludes
website/) and the full test suite against a locally started modules server
on port 6769: 161 passed, 4 skipped, 5 subtests passed, matching baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018dfYpyaSZd1nxSRLr8suj8
@elhoim elhoim changed the title fix: [website] register state-changing routes as POST, not GET [critical] fix: [website] register state-changing routes as POST, not GET Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant