This commit is contained in:
parent
33c68c2f44
commit
749f525f83
44 changed files with 5380 additions and 3532 deletions
89
tests/ShouldGuardRequestProvenance/main.cpp
Normal file
89
tests/ShouldGuardRequestProvenance/main.cpp
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
catcrafts.net
|
||||
Copyright (C) 2026 Catcrafts
|
||||
|
||||
The source code of this website is made available for viewing purposes only.
|
||||
No permission is granted to copy, modify, distribute, or create derivative works.
|
||||
*/
|
||||
|
||||
// Request provenance: the forwarded-address parser the rate limiter keys on,
|
||||
// and the Origin check that gates the order form.
|
||||
|
||||
import std;
|
||||
import Catcrafts.Shared;
|
||||
import Catcrafts.Server;
|
||||
|
||||
using namespace Catcrafts;
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void Check(bool ok, std::string_view what, std::string_view got = {}) {
|
||||
if (ok) return;
|
||||
++failures;
|
||||
std::println(std::cerr, "FAIL: {}{}{}", what,
|
||||
got.empty() ? "" : " got: ", got);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
// The rate limiter keys on this, so getting the WRONG end of the header
|
||||
// is not a cosmetic bug: the leftmost entry is client-controlled, and
|
||||
// trusting it would hand every attacker an endless supply of identities.
|
||||
{
|
||||
using Server::ClientAddressFromForwarded;
|
||||
Check(ClientAddressFromForwarded("203.0.113.7") == "203.0.113.7",
|
||||
"forwarded: single entry");
|
||||
Check(ClientAddressFromForwarded("198.51.100.4, 203.0.113.7") == "203.0.113.7",
|
||||
"forwarded: rightmost entry wins");
|
||||
// The attack this exists to defeat: a client that sends its own header
|
||||
// to look like a different peer. Caddy appends the truth on the right.
|
||||
Check(ClientAddressFromForwarded("1.1.1.1, 2.2.2.2, 203.0.113.7") == "203.0.113.7",
|
||||
"forwarded: spoofed prefix ignored");
|
||||
Check(ClientAddressFromForwarded("198.51.100.4, 203.0.113.7") == "203.0.113.7",
|
||||
"forwarded: padding trimmed");
|
||||
Check(ClientAddressFromForwarded("2001:db8::1") == "2001:db8::1",
|
||||
"forwarded: ipv6 passes through");
|
||||
Check(ClientAddressFromForwarded("").empty(), "forwarded: empty stays empty");
|
||||
// No header at all means nothing proxied this request; the caller must
|
||||
// see an empty peer and fall back to the global budget.
|
||||
Check(ClientAddressFromForwarded("198.51.100.4, ").empty(),
|
||||
"forwarded: empty last entry is no peer");
|
||||
}
|
||||
|
||||
{
|
||||
using Server::OriginAllowed;
|
||||
Check(OriginAllowed("https://catcrafts.net", "https://catcrafts.net"),
|
||||
"origin: same origin allowed");
|
||||
Check(OriginAllowed("https://catcrafts.net", "https://catcrafts.net/"),
|
||||
"origin: trailing slash on the base normalised");
|
||||
// A non-browser client (curl, the e2e suite) sends no Origin and
|
||||
// cannot be a cross-site forgery — there is no session to ride on.
|
||||
Check(OriginAllowed("", "https://catcrafts.net"), "origin: absent allowed");
|
||||
Check(!OriginAllowed("https://evil.example", "https://catcrafts.net"),
|
||||
"origin: foreign origin refused");
|
||||
// Neither a subdomain nor a lookalike is us.
|
||||
Check(!OriginAllowed("https://catcrafts.net.evil.example", "https://catcrafts.net"),
|
||||
"origin: suffix lookalike refused");
|
||||
Check(!OriginAllowed("https://shop.catcrafts.net", "https://catcrafts.net"),
|
||||
"origin: subdomain refused");
|
||||
// Scheme is part of an origin: http is not https.
|
||||
Check(!OriginAllowed("http://catcrafts.net", "https://catcrafts.net"),
|
||||
"origin: scheme mismatch refused");
|
||||
// A sandboxed iframe posts Origin: null. Present, and not us.
|
||||
Check(!OriginAllowed("null", "https://catcrafts.net"), "origin: null refused");
|
||||
Check(!OriginAllowed("https://catcrafts.net", ""),
|
||||
"origin: unconfigured base refuses rather than accepts all");
|
||||
// dev.sh serves on localhost and sets --redirect-base to match.
|
||||
Check(OriginAllowed("http://localhost:8080", "http://localhost:8080"),
|
||||
"origin: dev localhost base matches");
|
||||
}
|
||||
|
||||
if (failures != 0) {
|
||||
std::println(std::cerr, "{} check(s) failed", failures);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in a new issue