relay: HTTP bridge /query and /count don't cap filter count — filter-amplification DoS (WS door does)
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 76/100
Research direction
Read the filter-count guard in crates/buzz-relay/src/protocol.rs:93 and :131, then inspect the parsing sites in crates/buzz-relay/src/api/bridge.rs:974 and :1413. Verify that oversized filter lists sent to both POST /query and POST /count are rejected at the advertised limit of 10, while valid requests retain their existing behavior.
Written by the indexing model from the issue text.
Description
Summary
The HTTP bridge's POST /query and POST /count parse a NIP-01 filter list from the request body with no cap on the number of filters, while the WebSocket door enforces MAX_FILTERS_PER_REQ = 10 — the exact max_filters the relay advertises in NIP-11. Each filter becomes an independent DB query (or, for /count, an unbounded aggregate scan), so a single ≤1 MB request expands into hundreds of thousands of queries against the shared Postgres pool. One authenticated request per rate-limit tick is enough to monopolise the database.
Found on main @ 2ea9385.
The asymmetry
WebSocket REQ/COUNT reject over-long lists (crates/buzz-relay/src/protocol.rs:93, :131):
if filter_values.len() > MAX_FILTERS_PER_REQ { // = 10
return Err(RelayError::InvalidMessage(format!(
"REQ contains {} filters, maximum is {MAX_FILTERS_PER_REQ}", …)));
}
The HTTP bridge, which reaches the same query_events / count_events machinery, has no such check:
crates/buzz-relay/src/api/bridge.rs:974(/query):serde_json::from_slice(body)→Vec<Value>with no length guard.crates/buzz-relay/src/api/bridge.rs:1413(/count): same.
NIP-11 advertises max_filters: Some(10) (crates/buzz-relay/src/nip11.rs:110), so the bridge violates the relay's own advertised limit.
Reproduction
- Generate a throwaway keypair.
require_relay_membershipdefaults tofalse, and the open-relay path admits any authenticated key, so no registration is needed. - Build a ~1 MB body of empty filters:
[{},{},{}, …]— each empty filter is ~3 bytes, so ~349,000 fit under the 1 MBRequestBodyLimitLayer. - Sign a NIP-98
kind:27235event forPOST <relay>/queryand send it.
enforce_http_admission counts this as one API call against human_api_calls_per_min (default 300), so ~300 such requests/key/minute are allowed — and keys are free.
/query phase 1 builds ~349k EventQuery values; phase 2 runs them .buffered(FILTER_QUERY_CONCURRENCY) — bounded concurrency but unbounded total work — monopolising the pool; results are accumulated with no cross-filter dedupe. /count is cheaper still per filter: each fully-pushable filter runs an unbounded count_events aggregate, ~349k of them.
Impact
- MEDIUM DoS: a single ≤1 MB authenticated request drives ~10⁵–10⁶ DB operations, starving the shared pool and degrading the relay for the whole community. Amplification factor ~10⁵ per request; ~300 requests/key/min permitted.
- Cross-tenant blast radius is bounded (queries are community-scoped) but a single community's relay can be wedged by any of its authenticated participants.
Suggested fix
Apply the same cap the WS door defines, at both bridge parse sites — the relay already advertises it. PR attached.
(Originally surfaced during a security read of the relay's HTTP surface; the WS/bridge parity gap is the one reachable amplification I could confirm there.)
- Dominant language
- Rust
- Stars
- 33.7k
- Forks
- 4.4k
- Avg merge
- 1d 21h
- Merged PRs (30d)
- 239
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from block/buzz
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
workflow_sink's mention parser never masks code regions — @name inside a code span wakes the agent Open
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 Half a day Newbie friendliness 88/100
-
Difficulty 1/5 Under an hour Newbie friendliness 92/100
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
gitbutlerapp/gitbutler#15998 · 1 comment ·
-
bug triage:deciding
Difficulty 1/5 Under an hour Newbie friendliness 88/100
open-telemetry/otel-arrow#4132 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
bitcoindevkit/bdk-ffi#1125 ·