M-1 TokenExtract: exact-name cookie parse (new pure cookieValue helper) —
a substring find("session=") could be shadowed by a sibling xsession=,
defeating __Host-/__Secure- prefix guarantees.
M-2 AuthInterceptor: gate setup-mode pseudo-admin on a loopback bind and log
the grant; document that IAuthBackend::hasActiveUsers() must fail closed.
M-3 ws/Hub: empty propertyIds now means NO access for non-admins (was "all") —
a non-admin whose scope set failed to populate no longer gets every
property's notifications. Admins still get all via role.
M-4 new util/OriginCheck.hpp (originHostname/sameOrigin/originAllowed) +
Hub doc: WSController must validate Origin at the handshake (CSWSH).
M-6 RedactedFieldRepository: ctor throws on an unknown redaction field name
(a typo would silently redact nothing, leaving credentials in history).
M-7 RateLimiter: ctor validates capacity (finite >=1) / refillRate (finite >0),
throws std::invalid_argument — zero/negative/NaN silently disabled it.
M-8 TokenExtract: document that clientIpTrusted's "unknown"/"invalid" sentinels
collapse to one shared rate-limit bucket off-proxy.
M-9 new util/SessionCookie.hpp: safe-by-default Set-Cookie builder
(HttpOnly+Secure+SameSite=Strict+Path=/), rejects control chars / ';'.
M-10 AuthInterceptor: Origin/Referer-vs-Host check on session mutations
(defence in depth atop X-Requested-With); cert path documented as
non-browser / not CSRF-gated.
M-11 AuthInterceptor: optional injected RateLimiter throttles invalid-token
attempts per client IP → 429.
M-12 AuthInterceptor: sanitize request method/path (strip control chars, cap
length) before logging — closes log-line forging (CWE-117).
(M-5 — temporal non-atomic save — was already resolved by the H-4 fix.)
Tests: new test_token_extract / test_rate_limiter / test_origin_check /
test_session_cookie; extended test_redacted_field_repository. All 19 ctest
targets pass. README + header docs updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
67 lines
2.5 KiB
C++
67 lines
2.5 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_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_is_valid_ip();
|
|
std::printf("%s (%d failures)\n", g_failures ? "FAIL" : "OK", g_failures);
|
|
return g_failures ? 1 : 0;
|
|
}
|