Skip to content

fix(client): bound server-driven pagination in drain_cursor_pages - #42

Open
schenkty wants to merge 3 commits into
mainfrom
cursor/fix-client-pagination-unbounded-eb95
Open

schenkty wants to merge 3 commits into
mainfrom
cursor/fix-client-pagination-unbounded-eb95

Conversation

@schenkty

@schenkty schenkty commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Summary

drain_cursor_pages (used by the public chain_all, history_all, and global_history_all) accumulates pages from a remote representative and only terminates when the node returns an empty page or a None cursor — both entirely server-controlled. It appends server-controlled items each iteration with no cursor-progress check and no aggregate ceiling (the generic bound was only CURSOR: Copy).

A malicious, compromised, or MITM representative can therefore return a non-empty page plus Some(next_key) forever — e.g. by replaying one page under a constant cursor — driving unbounded memory growth or a non-terminating call in the client (DoS). For chain_all the appended blocks are only decoded (not quorum-verified), so replay is essentially free.

Reproduced with a standalone model of the loop: a mock fetch returning one item and a constant cursor appended 1,000,000 items without self-terminating.

Fix

  • Require the cursor to strictly advance (tighten the bound to CURSOR: Copy + PartialEq); a repeated cursor now returns an error instead of looping.
  • Add a MAX_DRAINED_ITEMS safety ceiling as a backstop against a node that fabricates advancing cursors forever.
  • Both conditions return a new ClientError::PaginationLimitExceeded (code PAGINATION_LIMIT_EXCEEDED).

Both cursor types in use (BlockHash, VoteBlockHash) already derive Copy + PartialEq, so no caller signature changes.

Note: MAX_DRAINED_ITEMS is a conservative constant (1,000,000). If a legitimate account/global history can exceed it, consider making it configurable (via ClientConfig) or having callers page explicitly through the existing *_page_cursor APIs.

Tests

cargo test -p keetanetwork-client, including:

  • drain_cursor_pages_rejects_a_non_advancing_cursor — a constant-cursor server yields PaginationLimitExceeded.
  • drain_cursor_pages_rejects_when_advancing_pages_exceed_the_item_ceiling — a server that keeps advancing cursors still trips MAX_DRAINED_ITEMS.
  • existing well-behaved paging tests still pass.

The Some(_) arm comment now describes a non-advancing cursor (not end of sequence).

Severity: medium (client memory exhaustion / non-terminating call).

Draft — audit fix; do not merge without maintainer review of the ceiling/config choice.

Open in Web Open in Cursor 

drain_cursor_pages accumulated pages from an untrusted representative until the
node returned an empty page or a None cursor - both server-controlled. With no
cursor-progress check and no ceiling, a malicious node that always returns a
non-empty page plus Some(next_key) (e.g. by replaying one page or echoing a
constant cursor) could drive unbounded memory growth or a non-terminating loop
in the client. Reached from the public chain_all/history_all/global_history_all.

Require the cursor to strictly advance (tighten CURSOR to Copy + PartialEq) and
add a MAX_DRAINED_ITEMS safety ceiling; both return the new
ClientError::PaginationLimitExceeded instead of looping/allocating without
bound.

Co-authored-by: Ty Schenk <schenkty@users.noreply.github.com>

@larseidsvoll larseidsvoll left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unbounded drain closed: cursor must advance + hard item ceiling + dedicated error. Nits: ceiling unit test; fix Some(_) comment. Approve for the security fix. Draft + Ty lock — no merge.

@larseidsvoll
larseidsvoll marked this pull request as ready for review September 16, 2026 00:50

@larseidsvoll larseidsvoll left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: APPROVE (re-stamp after #45)

Tip moved for main merge. Diff unchanged: cursor must advance + MAX_DRAINED_ITEMS + PaginationLimitExceeded.

Security Audit green. Lint green; Tests still running.

Review only — do not bot-merge. Humans merge. No @ humans.

@schenkty
schenkty requested review from rkeene and sephynox September 16, 2026 01:35
@larseidsvoll

Copy link
Copy Markdown

No security issues found.

Tip bdaa8a935091323e3f66750773e3f5b5b7e1a7cf: checked drain_cursor_pages cursor-advance guard, MAX_DRAINED_ITEMS, and PaginationLimitExceeded.

@larseidsvoll

Copy link
Copy Markdown

Thanks — nits noted from the APPROVE:

  1. Add a unit test that hits the MAX_DRAINED_ITEMS ceiling.
  2. Fix the stale Some(_) comment.

Will land those on this branch shortly.

Co-authored-by: Ty Schenk <schenkty@users.noreply.github.com>
@sonarqubecloud

Copy link
Copy Markdown

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.

3 participants