catcrafts.net/tests/ShouldGuardRequestProvenance/main.cpp

89 lines
3.9 KiB
C++
Raw Normal View History

2026-08-15 00:54:05 +02:00
/*
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;
}