relay: HTTP bridge /query and /count don't cap filter count — filter-amplification DoS (WS door does)

Open Beginner friendly
#4,985 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
76/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
postgresql, rust

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

  1. Generate a throwaway keypair. require_relay_membership defaults to false, and the open-relay path admits any authenticated key, so no registration is needed.
  2. Build a ~1 MB body of empty filters: [{},{},{}, …] — each empty filter is ~3 bytes, so ~349,000 fit under the 1 MB RequestBodyLimitLayer.
  3. Sign a NIP-98 kind:27235 event for POST <relay>/query and 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from block/buzz

All issues in block/buzz

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.