From 0436139c872150766779d6a80cdb4722e8969652 Mon Sep 17 00:00:00 2001 From: Uwe Schuster Date: Tue, 4 Aug 2026 04:22:44 +0200 Subject: [PATCH] extractToken: recognize the __Host-session cookie name (fix session regression) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `extractToken` only ever looked up `cookieValue(cookie, "session")`. After the M-1 fix switched cookie parsing from substring to exact-name matching (fafee12), a consumer that sets its session cookie under the hardened `__Host-session` name (fewo-webapp, #535) stopped resolving — every browser cookie request extracted an empty token and 401'd. The bare-`session` substring used to incidentally match inside `__Host-session=`; exact matching correctly no longer does. Fix: add `sessionCookieToken(cookieHeader)` which tries the bare `session` name and falls back to `__Host-session` (the `__Host-` prefix is strictly more secure, so recognizing it is safe), and route `extractToken` through it. The bare name is preferred when both are present. `cookieValue` keeps its exact generic semantics unchanged. Backward-compatible: consumers using `session=` are unaffected. Tests: new `test_session_cookie_token` covering both names, precedence, and substring traps. All 20 ctest targets pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/oatpp-authkit/util/TokenExtract.hpp | 27 ++++++++++++++++++--- test/test_token_extract.cpp | 23 ++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/include/oatpp-authkit/util/TokenExtract.hpp b/include/oatpp-authkit/util/TokenExtract.hpp index 6991701..98df4e2 100644 --- a/include/oatpp-authkit/util/TokenExtract.hpp +++ b/include/oatpp-authkit/util/TokenExtract.hpp @@ -47,17 +47,36 @@ inline std::string cookieValue(const std::string& cookieHeader, const std::strin return ""; } +/** + * @brief Read the session token from a `Cookie` header value under either the + * bare `session` name or the hardened `__Host-session` name. + * + * The `__Host-` cookie prefix (RFC 6265bis) is strictly *more* secure — it + * forces `Secure`, `Path=/` and host-only scoping — so a consumer that sets + * `__Host-session` must still be recognised here. `cookieValue` matches names + * exactly (see M-1), so the bare-`session` lookup does not match a + * `__Host-session` cookie; we fall back to the prefixed name explicitly. The + * bare name is preferred when both are present. This closes a regression where + * the M-1 exact-name parse silently stopped resolving `__Host-session` + * sessions (consumers renaming their cookie to `__Host-session` got 401s). + */ +inline std::string sessionCookieToken(const std::string& cookieHeader) { + std::string tok = cookieValue(cookieHeader, "session"); + if (tok.empty()) tok = cookieValue(cookieHeader, "__Host-session"); + return tok; +} + /** * @brief Pull the session token from an incoming request. * - * Order of precedence: `Cookie: session=...` → `Authorization: Bearer ...`. - * Returns "" when no token is present. Does not validate the token — callers - * hash it and look it up in their session store. + * Order of precedence: `Cookie: session=...` (or `__Host-session=...`) → + * `Authorization: Bearer ...`. Returns "" when no token is present. Does not + * validate the token — callers hash it and look it up in their session store. */ inline std::string extractToken(const std::shared_ptr& request) { auto cookie = request->getHeader("Cookie"); if (cookie && !cookie->empty()) { - std::string tok = cookieValue(*cookie, "session"); + std::string tok = sessionCookieToken(*cookie); if (!tok.empty()) return tok; } auto auth = request->getHeader("Authorization"); diff --git a/test/test_token_extract.cpp b/test/test_token_extract.cpp index 10b90a4..3049e5e 100644 --- a/test/test_token_extract.cpp +++ b/test/test_token_extract.cpp @@ -47,6 +47,28 @@ void test_cookie_exact_name_match() { REQUIRE(cookieValue("__Host-session=tok", "session") == ""); } +void test_session_cookie_token() { + // Bare `session` name resolves. + REQUIRE(sessionCookieToken("session=abc") == "abc"); + REQUIRE(sessionCookieToken("other=1; session=abc; more=2") == "abc"); + + // Regression: the `__Host-session` hardened name must also resolve — the + // exact-name parse (M-1) otherwise silently 401s consumers that renamed + // their session cookie to `__Host-session`. + REQUIRE(sessionCookieToken("__Host-session=tok") == "tok"); + REQUIRE(sessionCookieToken("other=1; __Host-session=tok") == "tok"); + REQUIRE(sessionCookieToken("__Host-session=tok; x=1") == "tok"); + + // The bare name is preferred when both are somehow present. + REQUIRE(sessionCookieToken("session=real; __Host-session=other") == "real"); + + // Substring traps still don't match either recognised name. + REQUIRE(sessionCookieToken("x__Host-session=evil") == ""); + REQUIRE(sessionCookieToken("xsession=evil") == ""); + REQUIRE(sessionCookieToken("") == ""); + REQUIRE(sessionCookieToken("foo=bar") == ""); +} + void test_is_valid_ip() { REQUIRE(isValidIp("192.168.1.1")); REQUIRE(isValidIp("::1")); @@ -61,6 +83,7 @@ void test_is_valid_ip() { int main() { test_cookie_exact_name_match(); + test_session_cookie_token(); test_is_valid_ip(); std::printf("%s (%d failures)\n", g_failures ? "FAIL" : "OK", g_failures); return g_failures ? 1 : 0;