Skip to content

fix: address 4 critical security and correctness issues#158

Merged
user1303836 merged 1 commit intomainfrom
fix/critical-issues-135-137-138-139
Mar 9, 2026
Merged

fix: address 4 critical security and correctness issues#158
user1303836 merged 1 commit intomainfrom
fix/critical-issues-135-137-138-139

Conversation

@user1303836
Copy link
Owner

Summary

Fixes four critical issues in a single surgical changeset:

  • security: unvalidated custom headers in webhook sink enable header injection #135 — Webhook header injection: Custom headers in webhook sink configs are now validated at job creation time. Forbidden header names (authorization, cookie, host, content-length, transfer-encoding, set-cookie, x-forwarded-for/proto/host) are blocked, header names must match [a-zA-Z0-9-_], and header values must not contain control characters (horizontal tab is allowed per HTTP spec).

  • bug: str_to_entry_type silently defaults unknown types to Transfer #137 — Silent default on unknown entry type: str_to_entry_type() now returns anyhow::Result<EntryType> instead of silently defaulting unknown values to Transfer. All call sites propagate the error. The test now verifies unknown types return an error.

  • bug: token_transfers dedup constraint includes amount, rejects legitimate records #138 — Dedup constraint includes amount / misses native_token: New migration (20260311100000_fix_dedup_constraints.sql) drops and recreates both unique indexes. token_transfers gets a transfer_index INT NOT NULL DEFAULT 0 column replacing amount in the constraint. native_balance_deltas adds native_token to its constraint. Rust insert queries, struct definitions, SELECT projections, and CSV export are updated accordingly.

  • bug: Hyperliquid/EVM parsers silently default malformed amounts to zero #139 — Silent zero on malformed amounts: All BigDecimal::from_str(...).unwrap_or_default() calls in hyperliquid_parser.rs and the hex_to_bigdecimal() function in evm_parser.rs now use proper error handling — logging a tracing::warn! with context (raw value, field name) and skipping the record instead of silently recording zero.

Files changed

  • api/src/main.rs — header validation in validate_sink_config(), new tests, transfer_index in TokenTransfer constructions and CSV export
  • adapters/src/repo.rsstr_to_entry_type returns Result, call site updated, tests updated
  • adapters/src/evm_parser.rshex_to_bigdecimal returns Result, call sites handle errors with warn+skip
  • adapters/src/hyperliquid_parser.rs — all unwrap_or_default() on BigDecimal::from_str replaced with warn+skip
  • adapters/src/v2_repo.rs — insert queries updated for transfer_index, ON CONFLICT clauses fixed
  • adapters/src/solana_parser.rstransfer_index tracking per (from, to, token) group
  • adapters/src/ledger_derivation.rstransfer_index field added to test helper
  • core/src/materializer.rstransfer_index: i32 added to TokenTransfer struct
  • migrations/20260311100000_fix_dedup_constraints.sql — new migration

Test plan

  • cargo fmt --all --check passes
  • cargo clippy --workspace --all-targets -- -D warnings passes
  • cargo test --workspace — 286 tests pass; 1 pre-existing failure (test_semaphore_limits_concurrent_jobs) confirmed on clean main
  • Verify migration applies cleanly against a running PostgreSQL instance
  • Verify webhook header validation rejects injection attempts end-to-end

Closes #135, closes #137, closes #138, closes #139

🤖 Generated with Claude Code

- #135: Validate custom headers in webhook sink to prevent header
  injection. Block forbidden header names (authorization, cookie, host,
  etc.), reject names with invalid characters, and reject values
  containing control characters.

- #137: Change str_to_entry_type to return anyhow::Result instead of
  silently defaulting unknown types to Transfer. Update all call sites
  to propagate errors, and update tests to verify error on unknown type.

- #138: Fix token_transfers and native_balance_deltas dedup constraints.
  Add transfer_index column to token_transfers and remove amount from
  the unique index. Add native_token to the native_balance_deltas unique
  index. Update Rust insert queries and struct definitions accordingly.

- #139: Replace unwrap_or_default() on BigDecimal parsing in Hyperliquid
  and EVM parsers with proper error handling. Malformed amounts now log
  a warning with context and skip the record instead of silently
  recording zero.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Owner Author

@user1303836 user1303836 left a comment

Choose a reason for hiding this comment

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

Review Complete — All Fixes Verified

Thorough review of all 9 changed files and the new migration. All four fixes are correct and well-implemented.

#135 — Webhook header injection

All 9 forbidden headers are blocked (case-insensitive). Header name validation restricts to [a-zA-Z0-9-_]. Header value validation blocks control characters except horizontal tab per HTTP spec. Validation is called in validate_sink_config() for the Webhook case. Five new tests cover forbidden headers, invalid names, control chars in values, valid custom headers, and tabs-allowed.

#137 — str_to_entry_type silent default

Returns anyhow::Result instead of silently defaulting to Transfer. The single production call site (line 333) propagates with ?. Test updated to verify error on unknown type. Roundtrip test updated.

#138 — Dedup constraints

Migration is idempotent (IF EXISTS/IF NOT EXISTS throughout). transfer_index INT NOT NULL DEFAULT 0 added to token_transfers. New unique index is (raw_transaction_id, from_address, to_address, token_address, transfer_index) — correctly excludes amount. native_balance_deltas unique index now includes native_token. All Rust insert queries, ON CONFLICT clauses, SELECT projections, CSV export, struct definitions, and test helpers are updated consistently. Solana parser correctly uses a HashMap counter to assign sequential transfer_index per (from, to, token) group within a transaction. EVM and HL parsers correctly use 0 since each produces at most one transfer per raw_transaction_id.

#139 — Silent zero defaults

All BigDecimal::from_str(...).unwrap_or_default() calls in hyperliquid_parser.rs are replaced. hex_to_bigdecimal() in evm_parser.rs now returns Result. All call sites log tracing::warn! with context (tx_hash, field name, raw value) and skip the record. For fee/pnl fields, the pattern of defaulting to zero and then checking != 0 effectively skips the record — same end result. The decode_evm_event_fields fallback to raw hex for informational event metadata is reasonable since those values are not used for financial calculations.

CI

  • cargo fmt --all --check — pass
  • cargo clippy --workspace --all-targets -- -D warnings — pass
  • cargo test --workspace — 286 passed, 1 failed (pre-existing test_semaphore_limits_concurrent_jobs on main)
  • GHA checks — all green

Ready to merge.

@user1303836 user1303836 merged commit 49b925b into main Mar 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant