Root cause
Authentication & access control introduced
The follow, unfollow, notification delete and notification read handlers were all registered as GET routes, so a page could reach them with a link or an image.
How AI contributed: Direct introductionPublished Aug 11, 2026 in
misp/cti-transmuteAll 4 findings in this repository
Follows and notifications were added in one change, and the four handlers that change follow and notification state were registered as GET routes. Each handler relies on the session of the current user and performs its change as soon as it is reached: following or unfollowing another account, deleting a notification, marking one notification read, or marking all of them read. Nothing in the handlers verifies a request token or the origin of the request, and a GET route is reachable from any page the signed-in user loads, including one controlled by an attacker. A link or an image tag is therefore enough to make the browser carry out the change on behalf of the victim. The fix re-registers the routes that create or remove state as POST and DELETE requests, which a browser does not issue from an image or a plain link.
The follow, delete-notification, mark-as-read and mark-all-read handlers were registered as GET routes on the account blueprint. They act on the session of the signed-in user, and none of them checks a request token or the origin of the request, which is what makes the state change reachable from elsewhere. A crafted link, an embedded image or any other cross-site request that the browser of a signed-in user follows performs the action on that user's account, so an attacker can follow or unfollow accounts and remove or clear notifications without the user doing anything.
Root cause
The follow, unfollow, notification delete and notification read handlers were all registered as GET routes, so a page could reach them with a link or an image.
Fix
The same handlers were re-registered to require POST or DELETE requests, so a plain link or image no longer reaches them.
Case facts
@@ -227,0 +237,22 @@ def delete_user(id) -> redirect:+@account_blueprint.route("/follow", methods=['GET'])+@login_required+def follow_user():+ """Follow or unfollow a user."""+ user_id = request.args.get('user_id', type=int)+ if not user_id:+ return {"success": False, "message": "Missing user_id", "toast_class": "danger"}, 400+ if user_id == current_user.id:+ return {"success": False, "message": "You cannot follow yourself", "toast_class": "warning"}, 400++ target = AccountModel.get_user(user_id)+ if not target:+ return {"success": False, "message": "User not found", "toast_class": "danger"}, 404++ already = AccountModel.is_following(current_user.id, user_id)+ if already:+ AccountModel.unfollow_user(current_user.id, user_id)+ return {"success": True, "following": False, "message": f"You unfollowed {target.first_name}", "toast_class": "info"}, 200+ else:+ AccountModel.follow_user(current_user.id, user_id)+ return {"success": True, "following": True, "message": f"You are now following {target.first_name}", "toast_class": "success"}, 200+@@ -339,7 +339,7 @@ def delete_user(id) -> redirect: # Follow / Unfollow # ########################### -@account_blueprint.route("/follow", methods=['GET'])+@account_blueprint.route("/follow", methods=['POST']) @login_required def follow_user(): """Follow or unfollow a user."""AI-assisted change ba66c847e1c722884b09b4cfc2cbad4b6983645a50bc0a252500d2111c124582 · Fix 93a336d84f50a8fa4f6974b838a57e3f13f25c91cb73b583394e75c682e6304f