From 3bd4f86f93da20922260e9f6af7e2e5a707c4c7e Mon Sep 17 00:00:00 2001 From: dawn <90008@klbr.net> Date: Sat, 27 Jun 2026 23:39:10 +0300 Subject: [PATCH] [beads] close issues that are not present or not issues in HEAD --- .beads/interactions.jsonl | 3 +++ .beads/issues.jsonl | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index c4d8f70..ec43a14 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -1 +1,4 @@ {"id":"int-9e82e018","kind":"field_change","created_at":"2026-06-27T20:31:50.656833364Z","actor":"dawn","issue_id":"hydrant-7ra","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"test"}} +{"id":"int-6eedbf38","kind":"field_change","created_at":"2026-06-27T20:34:46.528783549Z","actor":"dawn","issue_id":"hydrant-ct8","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"not an issue"}} +{"id":"int-55de70f4","kind":"field_change","created_at":"2026-06-27T20:34:46.630520455Z","actor":"dawn","issue_id":"hydrant-r6q","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"not an issue"}} +{"id":"int-ea64011f","kind":"field_change","created_at":"2026-06-27T20:39:01.288049505Z","actor":"dawn","issue_id":"hydrant-28z","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"resolved (ban.rs and BanTracker completely removed from codebase back in March)"}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 3d93396..536379a 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,9 +1,9 @@ -{"_type":"issue","id":"hydrant-ct8","title":"Default IPv6 bind exposes management API","description":"Finding: Default IPv6 bind exposes management API\nSeverity: HIGH\nOriginal Finding ID: 35ca6b23efd88191b6b48f02dafd0cd9\nIntroducing Commit: 82e340fdce9249e45f46d34150b8c0acea1c62c7\nRelevant Paths: src/main.rs | src/api/mod.rs | docs/getting-started.md\n\nDescription:\nHydrant's main API router contains both public and unauthenticated management endpoints. Before this commit, the binary read HYDRANT_API_PORT and bound only 0.0.0.0:\u003cport\u003e. The new default constructs two wildcard listeners: 0.0.0.0:3000 and [::]:3000. On IPv6-enabled hosts, this opens the full API surface on all IPv6 interfaces by default. Deployments that protected the old IPv4 listener with an IPv4 firewall, NAT rule, or reverse proxy can unintentionally expose the management API on a globally routed IPv6 address. Because those management endpoints include operations such as changing firehose sources, repository tracking, ingestion state, and DB maintenance, a remote attacker reaching the IPv6 listener can alter service behavior or cause denial of service without authentication. The change also ignores the old HYDRANT_API_PORT variable, increasing the chance that upgraded deployments unexpectedly fall back to the new wildcard default.","status":"open","priority":1,"issue_type":"bug","owner":"90008@klbr.net","created_at":"2026-06-27T20:31:58Z","created_by":"dawn","updated_at":"2026-06-27T20:31:58Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hydrant-ct8","title":"Default IPv6 bind exposes management API","description":"Finding: Default IPv6 bind exposes management API\nSeverity: HIGH\nOriginal Finding ID: 35ca6b23efd88191b6b48f02dafd0cd9\nIntroducing Commit: 82e340fdce9249e45f46d34150b8c0acea1c62c7\nRelevant Paths: src/main.rs | src/api/mod.rs | docs/getting-started.md\n\nDescription:\nHydrant's main API router contains both public and unauthenticated management endpoints. Before this commit, the binary read HYDRANT_API_PORT and bound only 0.0.0.0:\u003cport\u003e. The new default constructs two wildcard listeners: 0.0.0.0:3000 and [::]:3000. On IPv6-enabled hosts, this opens the full API surface on all IPv6 interfaces by default. Deployments that protected the old IPv4 listener with an IPv4 firewall, NAT rule, or reverse proxy can unintentionally expose the management API on a globally routed IPv6 address. Because those management endpoints include operations such as changing firehose sources, repository tracking, ingestion state, and DB maintenance, a remote attacker reaching the IPv6 listener can alter service behavior or cause denial of service without authentication. The change also ignores the old HYDRANT_API_PORT variable, increasing the chance that upgraded deployments unexpectedly fall back to the new wildcard default.","status":"closed","priority":1,"issue_type":"bug","owner":"90008@klbr.net","created_at":"2026-06-27T20:31:58Z","created_by":"dawn","updated_at":"2026-06-27T20:34:46Z","closed_at":"2026-06-27T20:34:46Z","close_reason":"not an issue","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"hydrant-jsk","title":"Unvalidated crawler sources enable SSRF and crawler DoS","description":"Finding: Unvalidated crawler sources enable SSRF and crawler DoS\nSeverity: MEDIUM\nOriginal Finding ID: 9f4f41a0383c8191a7bbf595fe317283\nIntroducing Commit: a3658e247c0692bcd7d38fe35f02efe92de56032\nRelevant Paths: src/api/mod.rs | src/api/crawler.rs | src/control.rs | src/crawler/mod.rs | src/crawler/relay.rs\n\nDescription:\nThe commit adds POST /crawler/sources to the main API router. The handler deserializes a url::Url and mode, but does not restrict the scheme, host, IP range, source count, or validate reachability before calling CrawlerHandle::add_source. add_source writes the URL to the database and spawns a producer task. The crawler base_url helper only rewrites ws/wss to http/https and otherwise leaves arbitrary schemes and hosts unchanged. In relay mode, request failures from the crawler source are logged and the loop immediately continues, so a source such as file:///x or an unreachable localhost/internal address can cause a tight error loop; an http(s) URL can also make the service repeatedly contact attacker-selected internal hosts with fixed XRPC paths. This is especially risky if management routes are reachable outside a trusted admin network; even though management APIs are intended to be private, this commit introduces a new persistent outbound-request primitive and amplifies a pre-existing no-backoff crawler error path.","status":"open","priority":2,"issue_type":"bug","owner":"90008@klbr.net","created_at":"2026-06-27T20:32:06Z","created_by":"dawn","updated_at":"2026-06-27T20:32:06Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"hydrant-r6q","title":"Unthrottled filtered Jetstream replay can exhaust DB I/O","description":"Finding: Unthrottled filtered Jetstream replay can exhaust DB I/O\nSeverity: MEDIUM\nOriginal Finding ID: b20c3936a0d88191bf76a5a88121a67a\nIntroducing Commit: e36574744e2bd5df2c602de4450ae0c4c101c517\nRelevant Paths: src/api/mod.rs | src/api/jetstream.rs | src/control/jetstream.rs | src/config.rs | src/control/stream.rs\n\nDescription:\nThe public `/subscribe` Jetstream endpoint accepts attacker-controlled cursors and filters. When a cursor is supplied, `jetstream_stream_thread` replays historical Jetstream events up to the current head. In the replay loop, the new pacing logic calls `replay_chunk_size_for()` and only blocks when the outbound mpsc channel is full. However, filters such as `wantedEventTypes=live` reject all historical replay events in `JetstreamSubscriberOptions::wants()`. In that case `jetstream_event_to_bytes()` returns `None`, no messages are sent, the outbound channel stays empty, and the channel-capacity backpressure never activates. Because this commit also changed the default `stream_replay_chunk_pause` from 2ms to zero, the replay loop immediately reads chunk after chunk from the DB until the replay window is exhausted. A remote unauthenticated client can open `/subscribe?cursor=0\u0026wantedEventTypes=live` and force a full-speed historical DB scan while receiving little or no data; multiple such connections can cause substantial disk I/O and CPU exhaustion. This should be throttled independently of output-channel backpressure, or historical replay should be skipped when filters can never match historical events.","status":"open","priority":2,"issue_type":"bug","owner":"90008@klbr.net","created_at":"2026-06-27T20:32:05Z","created_by":"dawn","updated_at":"2026-06-27T20:32:05Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hydrant-r6q","title":"Unthrottled filtered Jetstream replay can exhaust DB I/O","description":"Finding: Unthrottled filtered Jetstream replay can exhaust DB I/O\nSeverity: MEDIUM\nOriginal Finding ID: b20c3936a0d88191bf76a5a88121a67a\nIntroducing Commit: e36574744e2bd5df2c602de4450ae0c4c101c517\nRelevant Paths: src/api/mod.rs | src/api/jetstream.rs | src/control/jetstream.rs | src/config.rs | src/control/stream.rs\n\nDescription:\nThe public `/subscribe` Jetstream endpoint accepts attacker-controlled cursors and filters. When a cursor is supplied, `jetstream_stream_thread` replays historical Jetstream events up to the current head. In the replay loop, the new pacing logic calls `replay_chunk_size_for()` and only blocks when the outbound mpsc channel is full. However, filters such as `wantedEventTypes=live` reject all historical replay events in `JetstreamSubscriberOptions::wants()`. In that case `jetstream_event_to_bytes()` returns `None`, no messages are sent, the outbound channel stays empty, and the channel-capacity backpressure never activates. Because this commit also changed the default `stream_replay_chunk_pause` from 2ms to zero, the replay loop immediately reads chunk after chunk from the DB until the replay window is exhausted. A remote unauthenticated client can open `/subscribe?cursor=0\u0026wantedEventTypes=live` and force a full-speed historical DB scan while receiving little or no data; multiple such connections can cause substantial disk I/O and CPU exhaustion. This should be throttled independently of output-channel backpressure, or historical replay should be skipped when filters can never match historical events.","status":"closed","priority":2,"issue_type":"bug","owner":"90008@klbr.net","created_at":"2026-06-27T20:32:05Z","created_by":"dawn","updated_at":"2026-06-27T20:34:47Z","closed_at":"2026-06-27T20:34:47Z","close_reason":"not an issue","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"hydrant-3h9","title":"Unbounded listHosts seeding can exhaust relay resources","description":"Finding: Unbounded listHosts seeding can exhaust relay resources\nSeverity: MEDIUM\nOriginal Finding ID: 398f22beaf9081918de6e7c4f0d5dce5\nIntroducing Commit: 3ccb5332094f4b7e8215d6054e4532352b9be3a0\nRelevant Paths: src/config.rs | src/control/mod.rs | src/control/seed.rs | src/control/firehose.rs\n\nDescription:\nThis commit adds startup seeding from configured relay listHosts endpoints, enabled by default in relay builds. The seeding code fetches each listHosts page with resp.bytes(), which buffers the entire HTTP body without an explicit size limit, then consumes pagination until the remote endpoint stops returning a cursor. For every non-banned host in every response it constructs a WSS URL and calls firehose.add_source(), which persists the source and spawns a firehose ingestor task. A malicious or compromised configured seed endpoint, or poisoned listHosts data from an upstream relay, can therefore return oversized responses, endless cursors, or very large numbers of unique hostnames to cause memory/disk growth, unbounded persistent source creation, many Tokio tasks, and continuous outbound reconnect attempts. Host strings are also accepted directly as URL authority material without policy checks such as private-address filtering, but the clearest security impact is availability/resource exhaustion.","status":"open","priority":2,"issue_type":"bug","owner":"90008@klbr.net","created_at":"2026-06-27T20:32:04Z","created_by":"dawn","updated_at":"2026-06-27T20:32:04Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"hydrant-byv","title":"Unbounded describeRepo collection scan can OOM crawler","description":"Finding: Unbounded describeRepo collection scan can OOM crawler\nSeverity: MEDIUM\nOriginal Finding ID: 8804ed4fc0f481918a556492b330e29a\nIntroducing Commit: e263dacf6fb34fea58347054a8b3b22e22f10618\nRelevant Paths: src/crawler/mod.rs\n\nDescription:\nIn filtered crawler mode, unknown DIDs from the relay are passed to check_repo_signals. This commit changed the signal probe from per-signal com.atproto.repo.listRecords requests with limit=1 to a single com.atproto.repo.describeRepo request. describeRepo returns the repository's collection list, which is attacker-influenced and has no limit parameter. The code then calls resp.bytes() and serde_json::from_slice, buffering and deserializing the entire response before checking whether any configured signal is present. A malicious account/PDS discovered through normal crawling can return, or cause an honest PDS to return, an extremely large collections array, causing excessive memory/CPU use and potentially crashing the Hydrant process. This is distinct from the previous bounded query behavior because non-signal collections now have to be downloaded and parsed before the repo can be rejected.","status":"open","priority":2,"issue_type":"bug","owner":"90008@klbr.net","created_at":"2026-06-27T20:32:04Z","created_by":"dawn","updated_at":"2026-06-27T20:32:04Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"hydrant-28z","title":"Unbounded crawler PDS ban map enables memory DoS","description":"Finding: Unbounded crawler PDS ban map enables memory DoS\nSeverity: MEDIUM\nOriginal Finding ID: c2eefaff8ac08191afe7275e94231542\nIntroducing Commit: 106ab40683271705bf2096914ebe83075a01040f\nRelevant Paths: src/crawler/ban.rs | src/crawler/mod.rs | src/resolver.rs\n\nDescription:\nThis commit introduces BanTracker, a process-lifetime HashMap keyed by the full resolved PDS Url. check_repo_signals resolves each unknown DID to a PDS endpoint, then unconditionally calls tracker.get_handle(\u0026pds_url), which inserts a new State if that URL has not been seen before. Entries are never removed when a request succeeds, when a ban expires, or when the related DID is no longer relevant. Because crawler listRepos/DID documents are untrusted inputs in the threat model, an attacker controlling many DIDs or a malicious configured relay can provide many distinct PDS serviceEndpoint URLs and force unbounded in-memory state growth. Keying on the whole Url also means variants of the same origin, if accepted by DID document parsing, can create separate ban states while actual XRPC requests are joined to a fixed /xrpc path.","status":"open","priority":2,"issue_type":"bug","owner":"90008@klbr.net","created_at":"2026-06-27T20:32:03Z","created_by":"dawn","updated_at":"2026-06-27T20:32:03Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hydrant-28z","title":"Unbounded crawler PDS ban map enables memory DoS","description":"Finding: Unbounded crawler PDS ban map enables memory DoS\nSeverity: MEDIUM\nOriginal Finding ID: c2eefaff8ac08191afe7275e94231542\nIntroducing Commit: 106ab40683271705bf2096914ebe83075a01040f\nRelevant Paths: src/crawler/ban.rs | src/crawler/mod.rs | src/resolver.rs\n\nDescription:\nThis commit introduces BanTracker, a process-lifetime HashMap keyed by the full resolved PDS Url. check_repo_signals resolves each unknown DID to a PDS endpoint, then unconditionally calls tracker.get_handle(\u0026pds_url), which inserts a new State if that URL has not been seen before. Entries are never removed when a request succeeds, when a ban expires, or when the related DID is no longer relevant. Because crawler listRepos/DID documents are untrusted inputs in the threat model, an attacker controlling many DIDs or a malicious configured relay can provide many distinct PDS serviceEndpoint URLs and force unbounded in-memory state growth. Keying on the whole Url also means variants of the same origin, if accepted by DID document parsing, can create separate ban states while actual XRPC requests are joined to a fixed /xrpc path.","status":"closed","priority":2,"issue_type":"bug","owner":"90008@klbr.net","created_at":"2026-06-27T20:32:03Z","created_by":"dawn","updated_at":"2026-06-27T20:39:01Z","closed_at":"2026-06-27T20:39:01Z","close_reason":"resolved (ban.rs and BanTracker completely removed from codebase back in March)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"hydrant-7o8","title":"Unbounded collection enumeration in public describeRepo","description":"Finding: Unbounded collection enumeration in public describeRepo\nSeverity: MEDIUM\nOriginal Finding ID: b4a352096900819184744d6128f0583c\nIntroducing Commit: 6624ccf71b0e47458bbb555539d78c61f866eb02\nRelevant Paths: src/api/mod.rs | src/api/xrpc/mod.rs | src/api/xrpc/describe_repo.rs | src/control/repos.rs\n\nDescription:\nThe commit adds /xrpc/systems.gaze.hydrant.describeRepo to the public XRPC router. Handling a request resolves the identifier, obtains a repo handle, then concurrently calls repo.collections(). The collections() helper scans the entire counts keyspace prefix for that DID and inserts every matching collection into a HashMap. The handler then converts that into a HashSet for the JSON response, discarding the count values. There is no cap on scanned entries, response size, execution time, or memory use. Because collection-count entries are derived from indexed repository data, a malicious indexed account can create many distinct collection NSIDs and then any unauthenticated client can repeatedly call this public endpoint to force large DB prefix scans, allocations, and JSON serialization. This is a resource-exhaustion issue introduced by the new public endpoint.","status":"open","priority":2,"issue_type":"bug","owner":"90008@klbr.net","created_at":"2026-06-27T20:32:03Z","created_by":"dawn","updated_at":"2026-06-27T20:32:03Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"hydrant-0hs","title":"Stream event format change can trigger replay DoS","description":"Finding: Stream event format change can trigger replay DoS\nSeverity: MEDIUM\nOriginal Finding ID: 809b9f6d9b2c8191ad81c8da842aee68\nIntroducing Commit: a022ce698b1f3ddad6f88f35a4042f16063c3a33\nRelevant Paths: src/types.rs | src/api/stream.rs | src/api/mod.rs | src/ops.rs | src/backfill/mod.rs\n\nDescription:\n`StoredEvent` is stored in the database with `rmp_serde::to_vec` and replayed from `/stream` with `rmp_serde::from_slice`. `rmp-serde`'s default struct encoding is positional, so adding `live` at the start changes the on-disk field order. Older rows were encoded as `[did, rev, collection, rkey, action, cid]`; the new decoder expects `[live, did, rev, collection, rkey, action, cid]`. The `#[serde(default)]` on `live` does not protect this case because the old first element is present and is decoded as a boolean, which fails. In the stream handler, the cursor is advanced before deserialization, and deserialization failures are logged and skipped. Because `/stream` is publicly exposed, an attacker can connect with an old cursor such as `cursor=0` on an upgraded instance with many old-format events and repeatedly make the server scan the events keyspace and emit one error per legacy event, bypassing normal send-side backpressure for those skipped events. This also breaks historical replay integrity for legitimate clients.","status":"open","priority":2,"issue_type":"bug","owner":"90008@klbr.net","created_at":"2026-06-27T20:32:02Z","created_by":"dawn","updated_at":"2026-06-27T20:32:02Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"hydrant-8b8","title":"Public getRecord misses can cause disk-heavy DB reads","description":"Finding: Public getRecord misses can cause disk-heavy DB reads\nSeverity: MEDIUM\nOriginal Finding ID: 5a0782626f448191a4d162e1f6ca8438\nIntroducing Commit: a0a9359a895131783a843a7dd5327f2b8b7da0f5\nRelevant Paths: src/db/mod.rs | src/api/xrpc.rs\n\nDescription:\nThe records keyspace was changed from default Fjall options to `expect_point_read_hits(true)`. The added comment explicitly treats this as turning off Bloom filters because getRecord is assumed to be uncommon. However, `com.atproto.repo.getRecord` is publicly routed, accepts attacker-controlled repo/collection/rkey inputs, and directly performs a point lookup in `db.records` without first checking whether the repo or record exists. An unauthenticated client can therefore generate large numbers of guaranteed-missing record keys. On a large LSM keyspace, disabling miss filters makes those misses significantly more expensive because the database must do point-lookup work across on-disk state instead of cheaply rejecting absent keys. This can amplify public HTTP traffic into disk I/O and blocking-thread exhaustion.","status":"open","priority":2,"issue_type":"bug","owner":"90008@klbr.net","created_at":"2026-06-27T20:32:01Z","created_by":"dawn","updated_at":"2026-06-27T20:32:01Z","dependency_count":0,"dependent_count":0,"comment_count":0} -- 2.51.2