From 7c85837290c4e7d6f7d99472b092ad4f7c9d6a97 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 21 Aug 2026 12:55:24 +0200 Subject: [PATCH] :bug: Fix session invalidation on logout to prevent token replay (#11317) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Logout only cleared the auth-token cookie but never deleted the server-side row because delete-fn read ::id which wrap-authz no longer sets since 363b4e3778. Make delete-fn delete via ::session/:id attached by wrap-authz so replayed tokens are rejected (CWE-613, GHSA-mj9f-5cwq-7p3q). Add regression tests covering invalidation, idempotency and isolation of other sessions. Fix verified with Red→Green TDD and full backend suite (677 tests). Closes #11316 AI-assisted-by: muse-spark-1.2-contributor --- backend/src/app/http/session.clj | 2 +- backend/test/backend_tests/rpc_auth_test.clj | 106 +++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 backend/test/backend_tests/rpc_auth_test.clj diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index 61140a780c..5782b3452e 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -204,7 +204,7 @@ [{:keys [::manager]}] (assert (manager? manager) "expected valid session manager") (fn [request response] - (some->> (get request ::id) (delete-session manager)) + (some->> (get request ::session) :id (delete-session manager)) (clear-session-cookie response))) (defn decode-token diff --git a/backend/test/backend_tests/rpc_auth_test.clj b/backend/test/backend_tests/rpc_auth_test.clj new file mode 100644 index 0000000000..94adfb0d3a --- /dev/null +++ b/backend/test/backend_tests/rpc_auth_test.clj @@ -0,0 +1,106 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC Sucursal en España SL + +(ns backend-tests.rpc-auth-test + (:require + [app.common.uuid :as uuid] + [app.http.session :as session] + [backend-tests.helpers :as th] + [clojure.test :as t] + [yetti.response :as yres])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(t/deftest logout-invalidates-current-session + (let [prof (th/create-profile* 1) + manager (::session/manager th/*system*) + sid (uuid/random) + _ (th/db-exec-one! ["INSERT INTO http_session_v2 (id, profile_id, user_agent) VALUES (?, ?, ?)" + sid (:id prof) "test-agent"]) + session (session/read-session manager sid)] + + ;; Arrange: session exists before logout + (t/is (some? session) "session should exist before logout") + (t/is (= sid (:id session))) + + ;; Act: simulate Ring request as produced by wrap-authz (has ::session/session) + ;; delete-fn is used as response transform via rph/with-transform in auth/logout + (let [request {::session/session session} + response {} + delete-fn (session/delete-fn th/*system*) + result (delete-fn request response)] + + ;; Assert: server-side session is deleted (CWE-613) + (t/is (nil? (session/read-session manager sid)) + "session must be deleted server-side after logout (GHSA-mj9f-5cwq-7p3q)") + + ;; Assert: cookie is cleared + (t/is (= "" (get-in result [::yres/cookies "auth-token" :value])) + "auth-token cookie should be cleared") + (t/is (= 0 (get-in result [::yres/cookies "auth-token" :max-age])) + "auth-token cookie max-age should be 0")))) + +(t/deftest logout-clears-cookie-even-when-session-missing + (let [manager (::session/manager th/*system*) + sid (uuid/random) + ;; No session inserted, read should be nil + _ (t/is (nil? (session/read-session manager sid))) + request {} + response {} + delete-fn (session/delete-fn th/*system*) + result (delete-fn request response)] + + ;; Should still clear cookie (idempotent) + (t/is (= "" (get-in result [::yres/cookies "auth-token" :value]))) + (t/is (= 0 (get-in result [::yres/cookies "auth-token" :max-age]))))) + +(t/deftest logout-does-not-invalidate-other-sessions + (let [prof (th/create-profile* 1) + manager (::session/manager th/*system*) + sid1 (uuid/random) + sid2 (uuid/random) + _ (th/db-exec-one! ["INSERT INTO http_session_v2 (id, profile_id, user_agent) VALUES (?, ?, ?)" + sid1 (:id prof) "agent-1"]) + _ (th/db-exec-one! ["INSERT INTO http_session_v2 (id, profile_id, user_agent) VALUES (?, ?, ?)" + sid2 (:id prof) "agent-2"]) + s1 (session/read-session manager sid1) + s2 (session/read-session manager sid2)] + + (t/is (some? s1)) + (t/is (some? s2)) + + ;; Logout only sid1 + (let [request {::session/session s1} + response {} + delete-fn (session/delete-fn th/*system*)] + (delete-fn request response)) + + ;; sid1 deleted, sid2 intact + (t/is (nil? (session/read-session manager sid1)) "current session should be deleted") + (t/is (some? (session/read-session manager sid2)) "other sessions should remain"))) + +(t/deftest replay-after-logout-cannot-authenticate + (let [prof (th/create-profile* 1) + manager (::session/manager th/*system*) + sid (uuid/random) + _ (th/db-exec-one! ["INSERT INTO http_session_v2 (id, profile_id, user_agent) VALUES (?, ?, ?)" + sid (:id prof) "test-agent"]) + session (session/read-session manager sid)] + + (t/is (some? session) "session exists before logout") + + ;; Simulate logout + (let [request {::session/session session} + response {} + delete-fn (session/delete-fn th/*system*)] + (delete-fn request response)) + + ;; Replay: attempt to read session with same sid should fail (no profile attached) + (t/is (nil? (session/read-session manager sid)) + "replayed token must not resolve to a valid session after logout"))) + +