`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) <noreply@anthropic.com>
90 lines
3.6 KiB
C++
90 lines
3.6 KiB
C++
// Tests for oatpp-authkit/util/TokenExtract.hpp — exact-name cookie parsing
|
|
// (authkit#16 M-1) and isValidIp.
|
|
|
|
#include "oatpp-authkit/util/TokenExtract.hpp"
|
|
|
|
#include <cstdio>
|
|
#include <string>
|
|
|
|
namespace {
|
|
|
|
int g_failures = 0;
|
|
#define REQUIRE(expr) do { \
|
|
if (!(expr)) { \
|
|
std::fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \
|
|
++g_failures; \
|
|
} \
|
|
} while (0)
|
|
|
|
using namespace oatpp_authkit;
|
|
|
|
void test_cookie_exact_name_match() {
|
|
// Basic.
|
|
REQUIRE(cookieValue("session=abc", "session") == "abc");
|
|
REQUIRE(cookieValue("session=abc; other=1", "session") == "abc");
|
|
REQUIRE(cookieValue("other=1; session=abc", "session") == "abc");
|
|
REQUIRE(cookieValue("other=1; session=abc; more=2", "session") == "abc");
|
|
|
|
// OWS trimming around the pair and value.
|
|
REQUIRE(cookieValue("a=1; session=abc ; b=2", "session") == "abc");
|
|
|
|
// The substring trap: a prefixed/suffixed cookie name must NOT match.
|
|
REQUIRE(cookieValue("xsession=evil", "session") == "");
|
|
REQUIRE(cookieValue("notsession=evil", "session") == "");
|
|
REQUIRE(cookieValue("my_session=evil", "session") == "");
|
|
// Attacker plants a sibling cookie before the real one: exact match still
|
|
// returns the genuine session value, not the shadow.
|
|
REQUIRE(cookieValue("xsession=evil; session=real", "session") == "real");
|
|
REQUIRE(cookieValue("session=real; xsession=evil", "session") == "real");
|
|
|
|
// Missing / empty.
|
|
REQUIRE(cookieValue("", "session") == "");
|
|
REQUIRE(cookieValue("foo=bar", "session") == "");
|
|
REQUIRE(cookieValue("session=", "session") == "");
|
|
|
|
// __Host- prefixed name is matched only as an exact name.
|
|
REQUIRE(cookieValue("__Host-session=tok", "__Host-session") == "tok");
|
|
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"));
|
|
REQUIRE(isValidIp("2001:db8::1"));
|
|
REQUIRE(!isValidIp("192.168.1.256"));
|
|
REQUIRE(!isValidIp("1.1.1.1; rm -rf"));
|
|
REQUIRE(!isValidIp(""));
|
|
REQUIRE(!isValidIp(std::string(46, 'a'))); // over length cap
|
|
}
|
|
|
|
} // namespace
|
|
|
|
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;
|
|
}
|