Rate limiter: a token is accepted only after a 2xx; unaccepted tokens count against the IP (security H-1)

Before, any response other than 401 (426 without X-Kvitt-Protocol, 405, ...) marked a random bearer token accepted
for good: it never counted against its IP and was never pruned, so a stranger could bypass the 60/min IP limit and
grow the in-memory map without bound. Now (proposed D-115, SPEC §8.6): a request with a token that is not accepted
counts against its IP before it runs and is refused once the IP is over; a 2xx accepts the token and gives that
count back (a newly paired device costs its IP nothing, as before); a 401 for an accepted token counts against the
IP and withdraws the acceptance; token entries are pruned once their window expires, accepted or not. Every 401
still counts against the IP exactly once. Unit tests for each rule and an integration test for random tokens
without the protocol header (60 x 426, then 429, the paired device unaffected).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-18 22:40:02 +02:00
parent 21407456f1
commit 725901c98f
2 changed files with 181 additions and 35 deletions
@@ -96,6 +96,34 @@ async fn unknown_tokens_count_against_the_ip_but_known_devices_keep_working() {
assert_eq!(reply.status, 200);
}
#[tokio::test]
async fn random_tokens_without_the_protocol_header_are_limited_by_the_ip() {
// Answered 426 before any token check: such tokens are never accepted, so each request counts against the IP.
let app = TestApp::household().await;
assert_eq!(
app.send(from("192.0.2.1:1", None, "/me", Some(&admin_token())))
.await
.status,
200
);
let bare = |tok: &str| {
let mut request = from("192.0.2.1:1", None, "/me", Some(tok));
request.headers_mut().remove("x-kvitt-protocol");
request
};
for i in 0..60 {
let reply = app.send(bare(&format!("kvt_{i:0>43}"))).await;
assert_error(&reply, 426, "protocol_unsupported");
}
let reply = app.send(bare(&token('Z'))).await;
assert_error(&reply, 429, "rate_limited");
// The paired device keeps working from the same address.
let reply = app
.send(from("192.0.2.1:1", None, "/me", Some(&admin_token())))
.await;
assert_eq!(reply.status, 200);
}
#[tokio::test]
async fn x_forwarded_for_is_trusted_only_from_the_proxy() {
let app = TestApp::empty().await;