Skip to content

fix(audit): close 9 leak/DoS findings from 2026-05-13 security review#30

Merged
satyakwok merged 5 commits into
mainfrom
fix/audit-2026-05-13-leak-class
May 14, 2026
Merged

fix(audit): close 9 leak/DoS findings from 2026-05-13 security review#30
satyakwok merged 5 commits into
mainfrom
fix/audit-2026-05-13-leak-class

Conversation

@satyakwok
Copy link
Copy Markdown
Member

@satyakwok satyakwok commented May 14, 2026

Summary

Closes the CRITICAL + HIGH + MEDIUM findings from the 2026-05-13 security audit. LOW (request-id propagation, GraphQL depth limit) deferred.

CRITICAL

  • crates/api/src/graphql.rs:201-250 — sanitize sqlx errors in block / blocks / transaction resolvers via new sanitize_db_err() helper. Mirrors the REST-side ApiError::user_message() pattern; full error to tracing, generic "internal server error" to the client.
  • crates/api/src/lib.rs:119-138 + bin/api.rs:99-138/metrics no longer merged into the public router. New metrics_router() export bound by the bin to a separate loopback listener (default 127.0.0.1:9080, configurable via INDEXER_API_METRICS_BIND). Public Caddy proxy can no longer expose Prometheus metrics.

HIGH

  • crates/api/src/routes/etherscan.rs:90,141,144 — sanitize sqlx errors in txlist + getblocknobytime envelopes via new db_err() helper; tracing keeps the full error.
  • crates/api/src/routes/readyz.rs:34,44 — wire-side messages are static "down: pg error" / "down: cache error"; details go to tracing.
  • crates/coinblast/src/worker.rs:59,189-204 — cap known_non_curves at 10_000 entries (KNOWN_NON_CURVES_MAX); arbitrary eviction over cap via new cache_non_curve() helper. Stops unbounded HashSet growth.
  • crates/api/src/routes/etherscan.rs:35-92txlist now rejects non-default startblock / endblock / page with status=0 message="param not supported: ..." rather than silently ignoring (correctness bug).

MEDIUM

  • Cargo.toml:45 — sqlx workspace dep set to default-features = false + explicit feature list. cargo tree -e features | grep sqlx-mysql is empty post-change. RUSTSEC-2023-0071 (rsa Marvin) ignored in deny.toml + .github/workflows/cargo-audit.yml with a justification: sqlx-macros-core still lists sqlx-mysql as an optional dep so the resolver pins it in Cargo.lock even though no feature path enables compilation. Drop the ignore once sqlx upstream restructures.
  • crates/domain/src/ids.rs:23BlockHeight::as_u64() returns Option<u64> instead of panic! on negative. Sync uses BlockHeight(-1) as cursor sentinel; a future caller could have killed the worker. Call sites in crates/sync/src/block_writer.rs + crates/chain/src/provider.rs updated to either .expect("real chain height") (writer; receives a fully-resolved block) or surface ChainError::InvalidArgument (provider).
  • bin/api.rs:108-117 — documented the CorsLayer::permissive() assumption (no-auth public read API). Flag to revisit if mandatory auth is added.

Verification

  • RUSTFLAGS=\"-D warnings\" cargo build --release — passes
  • cargo test --workspace --no-fail-fast — all green (added block_height_sentinel_no_panic test)
  • cargo audit --ignore RUSTSEC-2023-0071 — clean (only pre-existing unmaintained paste / derivative warnings)

Test plan

  • Smoke: INDEXER_API_METRICS_BIND=127.0.0.1:9080 on staging — confirm /metrics serves on loopback only, public host returns 404.
  • Hit GraphQL block(height: 999999999) with PG down — response contains "internal server error" not raw sqlx text.
  • Hit /readyz with PG down — body has "pg": "down: pg error", tracing log carries the full error.
  • Etherscan ?module=account&action=txlist&address=0x...&startblock=100status=0 message="param not supported: startblock".
  • CoinBlast worker on a chain with >10k non-curve emitters — confirm RSS doesn't grow unboundedly.

Summary by CodeRabbit

  • New Features

    • Configurable metrics endpoint binding (default: 127.0.0.1:9080; can be disabled)
  • Bug Fixes

    • API, GraphQL and health endpoints no longer expose raw database/cache error details
    • Etherscan-compatible endpoints now reject unsupported query parameters and return generic errors
  • Improvements

    • Safer block-height validation and handling; tests updated
    • Bounded in-memory cache to prevent unbounded growth
  • Chores

    • Tightened dependency features and added RUSTSEC-2023-0071 to CI ignore list

Review Change Stack

CRITICAL
- graphql: sanitize sqlx errors (block/blocks/transaction); log full error
  internally, return generic "internal server error" to client.
- /metrics: split off main router; bind on 127.0.0.1:9080 by default
  (INDEXER_API_METRICS_BIND configurable). Public Caddy proxy can no
  longer expose Prometheus metrics.

HIGH
- etherscan: sanitize sqlx errors in txlist + getblocknobytime envelopes;
  log internally, return "database error".
- readyz: static "down: pg error" / "down: cache error" on the wire;
  details to tracing.
- coinblast worker: cap known_non_curves at 10_000 entries with arbitrary
  eviction over cap. Stops unbounded HashSet growth on chains with many
  spurious Buy/Sell-topic-shaped emitters.
- etherscan txlist: reject startblock/endblock/page when non-default
  rather than silently ignoring (returned wrong-window results).

MEDIUM
- sqlx workspace dep: default-features = false + explicit feature list;
  drops `any` so resolver no longer pulls sqlx-mysql via macros-core
  during compilation. cargo audit RUSTSEC-2023-0071 (rsa Marvin) ignored
  with a justification — sqlx-macros-core still lists sqlx-mysql as an
  optional dep so it stays in Cargo.lock even though never compiled.
- BlockHeight::as_u64 returns Option<u64> instead of panicking on
  negative. Sync uses BlockHeight(-1) as cursor sentinel; previously a
  future caller could panic-kill the worker. Provider/block_writer call
  sites updated to handle None explicitly.
- bin/api.rs: documented the CorsLayer::permissive() assumption (no-auth
  public read API); flag for revisit if mandatory auth is added.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 417b6555-e192-409e-9d83-b06dab50f9c6

📥 Commits

Reviewing files that changed from the base of the PR and between de37b74 and e2da72e.

📒 Files selected for processing (1)
  • crates/coinblast/src/worker.rs

📝 Walkthrough

Walkthrough

This PR hardens the indexer API and processing layers: it sanitizes database and cache errors to generic client responses while logging details, moves the metrics endpoint to a dedicated loopback-only listener with a new metrics_router, makes BlockHeight::as_u64() fallible and updates callers to validate heights, bounds an in-memory cache in coinblast, and constrains SQLx features while adding RUSTSEC-2023-0071 to advisory ignores.

Sequence Diagram(s)

sequenceDiagram
  participant ApiServer
  participant TcpListener
  participant MetricsRouter
  participant Client
  ApiServer->>TcpListener: bind(INDEXER_API_METRICS_BIND) (loopback-only)
  Client->>TcpListener: connect / request /metrics
  TcpListener->>MetricsRouter: dispatch request
  MetricsRouter->>Client: metrics response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Sentriscloud/indexer-rs#27: Introduces the base cargo audit GitHub Actions workflow that this PR extends with additional advisory suppressions.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description does not follow the repository's required template structure. Required sections like 'Scope' checkboxes and 'Checks' (build/test/fmt/slither/storage) are completely missing. Fill in all required template sections: add Scope checkboxes indicating this is not a contract change, complete the Checks list, specify the Linked issue, and select Deploy impact (likely 'No on-chain change' since changes are API/infra).
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: closing security audit findings (9 leak/DoS findings from the 2026-05-13 security review).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/audit-2026-05-13-leak-class

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bin/api.rs`:
- Around line 46-53: Parse and validate the indexer_api_metrics_bind value as a
SocketAddr early (the config field indexer_api_metrics_bind /
default_metrics_bind and the code that spawns the Prometheus metrics listener)
and reject any non-loopback addresses before creating the listener: use
std::net::SocketAddr::from_str (or to_socket_addrs) to parse the bind string,
check socket.ip().is_loopback(), and return an error or disable metrics if
false; ensure the validation happens at config/initialization time (before the
listener spawn) and surface a clear error mentioning INDEXER_API_METRICS_BIND so
callers cannot bind 0.0.0.0 or any external interface.

In `@crates/api/src/lib.rs`:
- Around line 121-126: The code currently drops RouterConfig.metrics_handle
silently (let _ = cfg.metrics_handle;) which hides an important breaking change;
update make_router (or the compatibility shim handling RouterConfig) to detect
when cfg.metrics_handle is Some(_) and either log a clear warning via the
existing logger or return/raise an error (fail fast) indicating /metrics is no
longer exposed, referencing RouterConfig.metrics_handle and metrics_router so
callers see the explicit behavior change; ensure the chosen behavior is
consistent with project conventions (use process or crate logger if available,
or return Err from make_router) so callers are not silently downgraded.

In `@crates/api/src/routes/etherscan.rs`:
- Around line 88-91: The return statement that builds the unsupported-param
error is formatted in a way that fails rustfmt; update the expression that uses
format! and unsupported.join to a single, properly formatted call (e.g. ensure
the join uses ", " and the format! call is on one line) so the return
err(format!(...)) expression in etherscan.rs compiles with cargo fmt --check;
locate the use of err, format!, and unsupported.join and reformat that return
accordingly.

In `@crates/sync/src/block_writer.rs`:
- Line 67: The single long chained call for the block_height field
(block_height: b.block.height.as_u64().expect("written block heights are
non-negative"),) exceeds rustfmt line length; break the chain across lines or
introduce a temporary variable. For example, compute let height =
b.block.height.as_u64().expect("written block heights are non-negative"); then
use block_height: height, or place the call on multiple lines like
b.block.height.as_u64() .expect("written block heights are non-negative"), to
satisfy cargo fmt --check in block_writer.rs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 00dc29b9-94b1-4b57-8334-1b758a99363b

📥 Commits

Reviewing files that changed from the base of the PR and between c5d2122 and c030b2b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/*.lock
📒 Files selected for processing (12)
  • .github/workflows/cargo-audit.yml
  • Cargo.toml
  • bin/api.rs
  • crates/api/src/graphql.rs
  • crates/api/src/lib.rs
  • crates/api/src/routes/etherscan.rs
  • crates/api/src/routes/readyz.rs
  • crates/chain/src/provider.rs
  • crates/coinblast/src/worker.rs
  • crates/domain/src/ids.rs
  • crates/sync/src/block_writer.rs
  • deny.toml

Comment thread bin/api.rs
Comment thread crates/api/src/lib.rs Outdated
Comment thread crates/api/src/routes/etherscan.rs Outdated
Comment thread crates/sync/src/block_writer.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/sync/src/block_writer.rs`:
- Around line 67-71: The current code uses b.block.height.as_u64().expect(...)
which can panic; change it to a non-panicking, best-effort extraction (e.g.,
map_or / unwrap_or / match on as_u64()) and substitute a safe fallback value
(like 0 or None) instead of panicking, and emit a warning/metric via the same
observability path (logger/telemetry) when the height cannot be converted;
update the block_height assignment (the expression starting with
b.block.height.as_u64()) to perform this graceful handling so analytics remain
non-fatal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 99bf51fa-e4c0-4f79-8e5b-ea1ed215cde3

📥 Commits

Reviewing files that changed from the base of the PR and between c030b2b and 3424488.

📒 Files selected for processing (2)
  • crates/api/src/routes/etherscan.rs
  • crates/sync/src/block_writer.rs

Comment thread crates/sync/src/block_writer.rs Outdated
CR review on PR #30:
- bin/api.rs: parse INDEXER_API_METRICS_BIND at startup; bail if not
  loopback (was a path back to the unauth-public-metrics exposure
  this PR was meant to close).
- crates/api/src/lib.rs: tracing::warn when caller still passes
  RouterConfig.metrics_handle so the silent-drop is visible in logs.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bin/api.rs`:
- Line 133: The code currently calls tokio::net::TcpListener::bind(&bind).await
which re-parses the address string; use the already-parsed SocketAddr variable
(parsed) instead to avoid re-parsing and potential panics—replace the bind call
to pass parsed (the SocketAddr validated earlier) directly to TcpListener::bind
so it uses the existing parsed value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1b15f8c4-3b01-4a47-88d7-87acef25b565

📥 Commits

Reviewing files that changed from the base of the PR and between 3424488 and 961ae97.

📒 Files selected for processing (2)
  • bin/api.rs
  • crates/api/src/lib.rs

Comment thread bin/api.rs
}
let mh = metrics_handle.clone();
tokio::spawn(async move {
match tokio::net::TcpListener::bind(&bind).await {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Use the already-parsed SocketAddr instead of re-parsing the string.

parsed is already validated at lines 121-124. Passing it directly to TcpListener::bind() avoids re-parsing and is cleaner.

Suggested change
         let mh = metrics_handle.clone();
+        let bind_addr = parsed;
         tokio::spawn(async move {
-            match tokio::net::TcpListener::bind(&bind).await {
+            match tokio::net::TcpListener::bind(bind_addr).await {
                 Ok(listener) => {
-                    tracing::info!(addr = %bind, "api: metrics listener up (loopback)");
+                    tracing::info!(addr = %bind_addr, "api: metrics listener up (loopback)");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
match tokio::net::TcpListener::bind(&bind).await {
let mh = metrics_handle.clone();
let bind_addr = parsed;
tokio::spawn(async move {
match tokio::net::TcpListener::bind(bind_addr).await {
Ok(listener) => {
tracing::info!(addr = %bind_addr, "api: metrics listener up (loopback)");
if let Err(e) =
axum::serve(listener, metrics_router(mh).into_make_service()).await
{
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/api.rs` at line 133, The code currently calls
tokio::net::TcpListener::bind(&bind).await which re-parses the address string;
use the already-parsed SocketAddr variable (parsed) instead to avoid re-parsing
and potential panics—replace the bind call to pass parsed (the SocketAddr
validated earlier) directly to TcpListener::bind so it uses the existing parsed
value.

satyakwok added 2 commits May 14, 2026 12:02
CR review on PR #30:
- block_writer.rs: replace .expect() on block height conversion with
  match → warn-and-skip pattern. Analytics is best-effort already
  (comment line 62-63); don't escalate a never-hit-but-possible
  conversion miss to a worker panic.
CR-pushed clippy strict-warnings: collapsible_if on the LRU eviction
guard. Use let-chain syntax so the .cloned() doesn't fire when set is
under cap.
@satyakwok satyakwok merged commit 1c21dba into main May 14, 2026
7 of 8 checks passed
@satyakwok satyakwok deleted the fix/audit-2026-05-13-leak-class branch May 14, 2026 10:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant