fix: address 4 critical security and correctness issues#158
Conversation
- #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>
user1303836
left a comment
There was a problem hiding this comment.
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— passcargo clippy --workspace --all-targets -- -D warnings— passcargo test --workspace— 286 passed, 1 failed (pre-existingtest_semaphore_limits_concurrent_jobson main)- GHA checks — all green
Ready to merge.
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 returnsanyhow::Result<EntryType>instead of silently defaulting unknown values toTransfer. 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_transfersgets atransfer_index INT NOT NULL DEFAULT 0column replacingamountin the constraint.native_balance_deltasaddsnative_tokento 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 inhyperliquid_parser.rsand thehex_to_bigdecimal()function inevm_parser.rsnow use proper error handling — logging atracing::warn!with context (raw value, field name) and skipping the record instead of silently recording zero.Files changed
api/src/main.rs— header validation invalidate_sink_config(), new tests,transfer_indexin TokenTransfer constructions and CSV exportadapters/src/repo.rs—str_to_entry_typereturnsResult, call site updated, tests updatedadapters/src/evm_parser.rs—hex_to_bigdecimalreturnsResult, call sites handle errors with warn+skipadapters/src/hyperliquid_parser.rs— allunwrap_or_default()onBigDecimal::from_strreplaced with warn+skipadapters/src/v2_repo.rs— insert queries updated fortransfer_index, ON CONFLICT clauses fixedadapters/src/solana_parser.rs—transfer_indextracking per (from, to, token) groupadapters/src/ledger_derivation.rs—transfer_indexfield added to test helpercore/src/materializer.rs—transfer_index: i32added toTokenTransferstructmigrations/20260311100000_fix_dedup_constraints.sql— new migrationTest plan
cargo fmt --all --checkpassescargo clippy --workspace --all-targets -- -D warningspassescargo test --workspace— 286 tests pass; 1 pre-existing failure (test_semaphore_limits_concurrent_jobs) confirmed on cleanmainCloses #135, closes #137, closes #138, closes #139
🤖 Generated with Claude Code