89 lines
3.9 KiB
C++
89 lines
3.9 KiB
C++
|
|
/*
|
||
|
|
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;
|
||
|
|
}
|