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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user