Skip to content

fix(Rosetta): DEFI-2902: Enforce a 24h ingress window in Rosetta /construction/payloads#10547

Open
mbjorkqvist wants to merge 4 commits into
masterfrom
mathias/DEFI-2902-ingress-window-validation
Open

fix(Rosetta): DEFI-2902: Enforce a 24h ingress window in Rosetta /construction/payloads#10547
mbjorkqvist wants to merge 4 commits into
masterfrom
mathias/DEFI-2902-ingress-window-validation

Conversation

@mbjorkqvist

@mbjorkqvist mbjorkqvist commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Why

The /construction/payloads endpoint of both ICP Rosetta and ICRC1 Rosetta accepts caller-controlled ingress_start/ingress_end values and expands them into a list of ingress expiries without enforcing the 24-hour bound documented on ConstructionPayloadsRequestMetadata. This lets an unauthenticated request drive the expiry loop to exhaust memory and crash an operator's Rosetta instance.

Two distinct payloads trigger it: an oversized window (a huge ingress_end - ingress_start span), and a near-u64::MAX window whose span is tiny but whose increment wraps so the loop never terminates.

What

validate_ingress_window now runs before the loop in both services and rejects a request when either:

  • the span ingress_end - ingress_start exceeds 24h, or
  • ingress_end is more than 24h in the future.

The second bound is what closes the wraparound payload — its span is sub-minute, so a span-only check would not catch it.

The change is built up across three commits: a regression test characterizing the current (unbounded) behavior, the enforcement itself, and the flip of those tests to assert rejection plus a negative control and a boundary case. The existing roundtrip proptests are adapted to the new behavior.

Scope is limited to operator-deployed Rosetta instances; IC mainnet, NNS, consensus and ledger canisters are unaffected.

mbjorkqvist and others added 3 commits June 23, 2026 12:59
…truction/payloads

Add a `validate_ingress_window` seam (currently a no-op) before the ingress
expiry loop in both ICP Rosetta (`construction_payloads.rs`) and ICRC1 Rosetta
(`services.rs`), plus regression tests that document the reported (ICPBB-134 /
DEFI-2902) behavior: oversized windows, unbounded spans and the near-`u64::MAX`
wrap payload are all currently accepted without any bound being enforced.

These tests pass against current behavior; the enforcement and the assertion
flip follow in subsequent commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enforce the documented 24-hour ingress window before the expiry loop in both
ICP Rosetta and ICRC1 Rosetta `/construction/payloads`. `validate_ingress_window`
now rejects requests where:
- the span `ingress_end - ingress_start` exceeds 24h (closes the unbounded-loop
  / OOM vector with a large window), or
- `ingress_end` is more than 24h in the future (closes the near-`u64::MAX`
  wraparound vector, whose span is tiny but whose end is absurdly far ahead).

A span-only bound is insufficient on its own: the wrap payload uses a sub-minute
span, so the future-bound check is what makes it unreachable.

The existing roundtrip proptests are adapted to the new behavior: the ICRC1
`test_construction_parse` now expects rejection for out-of-bound windows, and the
ICP `test_payloads_parse_identity` generator keeps `ingress_start` within a
realistic window around now (which also removes a latent `ingress_start +
ingress_interval` overflow).

The characterization tests added in the previous commit now fail (they assert the
old, unbounded behavior); they are updated to assert rejection in the next commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flip the characterization tests to assert that oversized windows, unbounded
spans and the near-`u64::MAX` wrap payload are now rejected, and add a negative
control (a realistic 5-minute window is accepted) plus a boundary test (exactly
24h is accepted, one nanosecond more is rejected) in both ICP and ICRC1 Rosetta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the Rosetta /construction/payloads endpoints (ICP + ICRC1) by enforcing the documented 24-hour ingress window constraints before expanding caller-provided ingress timestamps into per-interval ingress expiries, preventing unauthenticated DoS via excessive or non-terminating expiry loops (ICPBB-134 / DEFI-2902).

Changes:

  • Added validate_ingress_window checks to reject ingress windows with span > 24h and/or ingress_end > 24h in the future (wraparound defense).
  • Introduced MAX_INGRESS_WINDOW constant (ICRC1) and added targeted unit tests for oversized and wraparound vectors (ICP + ICRC1).
  • Updated proptests to accommodate the newly enforced bounds.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
rs/rosetta-api/icrc1/src/construction_api/services.rs Adds ingress-window validation + tests; enforces checks before expiry expansion.
rs/rosetta-api/icrc1/src/common/constants.rs Introduces shared MAX_INGRESS_WINDOW (24h) constant for ICRC1 Rosetta.
rs/rosetta-api/icp/src/request_handler/construction_payloads.rs Adds ingress-window validation in ICP Rosetta and unit tests.
rs/rosetta-api/icp/src/request_handler/construction_parse.rs Adjusts proptest metadata generation to stay within the new ingress-window constraints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rs/rosetta-api/icp/src/request_handler/construction_payloads.rs
Comment thread rs/rosetta-api/icrc1/src/construction_api/services.rs
- ICP `validate_ingress_window` now rejects `ingress_start >= ingress_end`
  before the span/future checks. Without this, the `saturating_sub`-based span
  check treated a reversed/empty window as a zero-length span that passed
  validation, yielding an empty set of ingress expiries (ICP, unlike ICRC1, had
  no prior start/end ordering guard). Adds a regression test.
- The ICRC1 `test_construction_parse` proptest now calls `validate_ingress_window`
  directly instead of duplicating the bound logic inline, so the test cannot
  drift from production validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mbjorkqvist mbjorkqvist marked this pull request as ready for review June 23, 2026 18:03
@mbjorkqvist mbjorkqvist requested a review from a team as a code owner June 23, 2026 18:03
@github-actions github-actions Bot added the @defi label Jun 23, 2026
@mbjorkqvist mbjorkqvist changed the title fix: enforce a 24h ingress window in Rosetta /construction/payloads (DEFI-2902) fix(Rosetta): DEFI-2902: Enforce a 24h ingress window in Rosetta /construction/payloads Jun 23, 2026
@zeropath-ai

zeropath-ai Bot commented Jun 23, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to bd5d728.

Security Overview
Detected Code Changes

| Change Type | Relevant files

... (code changes summary truncated to fit VCS comment limits.)

@gregorydemay gregorydemay left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @mbjorkqvist ! Just a comment asking why the validating functions are slightly different.

Time::from_nanos_since_unix_epoch(nanos)
}

// Vector A (ICPBB-134): a window far larger than the documented 24h bound is

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: do we want the bugbounty number to appear in the code? Maybe better to keep it only in the Jira ticket?

/// - `ingress_end` must not be more than 24h in the future, which also rejects
/// the near-`u64::MAX` payloads that would otherwise wrap the loop counter.
fn validate_ingress_window(now: u64, ingress_start: u64, ingress_end: u64) -> Result<(), Error> {
let max_window = MAX_INGRESS_WINDOW.as_nanos() as u64;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Contrary to the validating function in ICP Rosetta the check

    if start >= end {
        return Err(ApiError::invalid_request(
            "ingress_start must be strictly before ingress_end.",
        ));
    }

seems to be missing, is that on purpose?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants