fix(relay): bound HTTP filter work (BUZZ-SEC-028)

This commit is contained in:
Jordan Mecom
2026-07-16 11:47:56 -07:00
parent 44359f635a
commit bbfb6b5d5c
2 changed files with 51 additions and 1 deletions
+50
View File
@@ -669,6 +669,7 @@ pub async fn query_events(
// depth_limit, feed_types) that nostr::Filter silently drops.
let raw_filters: Vec<Value> = serde_json::from_slice(&body)
.map_err(|e| api_error(StatusCode::BAD_REQUEST, &format!("invalid filters: {e}")))?;
enforce_bridge_filter_limit(raw_filters.len())?;
let filters: Vec<nostr::Filter> = raw_filters
.iter()
.map(|v| serde_json::from_value(v.clone()))
@@ -999,6 +1000,7 @@ pub async fn query_events(
}
}
deduplicate_event_values(&mut events);
Ok(Json(Value::Array(events)))
}
@@ -1050,6 +1052,7 @@ pub async fn count_events(
let filters: Vec<nostr::Filter> = serde_json::from_slice(&body)
.map_err(|e| api_error(StatusCode::BAD_REQUEST, &format!("invalid filters: {e}")))?;
enforce_bridge_filter_limit(filters.len())?;
// P-gated kinds enforcement — same as WS REQ and /query.
let authed_pubkey_hex = pubkey.to_hex();
@@ -1227,6 +1230,29 @@ pub async fn count_events(
Ok(Json(serde_json::json!({ "count": total })))
}
fn enforce_bridge_filter_limit(filter_count: usize) -> Result<(), (StatusCode, Json<Value>)> {
if filter_count > crate::protocol::MAX_FILTERS_PER_REQ {
return Err(api_error(
StatusCode::BAD_REQUEST,
&format!(
"request contains {filter_count} filters, maximum is {}",
crate::protocol::MAX_FILTERS_PER_REQ
),
));
}
Ok(())
}
fn deduplicate_event_values(events: &mut Vec<Value>) {
let mut seen_ids = std::collections::HashSet::new();
events.retain(|event| {
event
.get("id")
.and_then(Value::as_str)
.is_none_or(|id| seen_ids.insert(id.to_string()))
});
}
fn has_mixed_search_filters(filters: &[nostr::Filter]) -> bool {
filters.iter().any(|f| f.search.is_some()) && filters.iter().any(|f| f.search.is_none())
}
@@ -1890,6 +1916,30 @@ mod tests {
assert!(!has_mixed_search_filters(&filters));
}
#[test]
fn bridge_filter_budget_bounds_work_and_duplicate_output() {
assert!(enforce_bridge_filter_limit(crate::protocol::MAX_FILTERS_PER_REQ).is_ok());
let (status, response) =
enforce_bridge_filter_limit(crate::protocol::MAX_FILTERS_PER_REQ + 1)
.expect_err("eleven filters must be rejected before query execution");
assert_eq!(status, StatusCode::BAD_REQUEST);
assert!(response["error"]
.as_str()
.is_some_and(|message| message.contains("maximum is 10")));
let mut events = vec![
serde_json::json!({"id": "same", "content": "first"}),
serde_json::json!({"id": "same", "content": "duplicate"}),
serde_json::json!({"id": "other", "content": "second"}),
];
deduplicate_event_values(&mut events);
assert_eq!(events.len(), 2);
assert_eq!(events[0]["content"], "first");
assert_eq!(events[1]["id"], "other");
}
#[test]
fn bridge_search_mode_extension_defaults_to_full_text() {
assert_eq!(
+1 -1
View File
@@ -9,7 +9,7 @@ use crate::error::{RelayError, Result};
const MAX_SUB_ID_LENGTH: usize = 256;
/// NIP-11 advertised limit: REQ messages with more filters than this are rejected.
const MAX_FILTERS_PER_REQ: usize = 10;
pub(crate) const MAX_FILTERS_PER_REQ: usize = 10;
/// A message sent by a NIP-01 client to the relay.
#[derive(Debug, Clone)]