Timing-Safe Cursor Pagination for Public Unauthenticated Marketplace APIs: HMAC Signing, Dual-Consent Visibility, and Enumerated Projection
Abstract
Public unauthenticated APIs that surface multi-tenant marketplace data face a compound security problem: pagination cursors must not enable enumeration of private records, visibility must require consent from both the resource's owner and any cross-referenced parties, and field projection must prevent private-data escape through SELECT-list drift. Each individual primitive (HMAC-signed cursors, consent flags, whitelist projection) is well-understood; their compound deployment under adversarial-parallel verification is not. We describe the compound pattern deployed in production for a multi-tenant agent marketplace: HMAC-SHA256 cursor signing via the Web Crypto API for timing-safety guarantees, dual-consent visibility predicates evaluated at SQL read time, enumerated whitelist projection forbidding spread operators, and rate limiting via per-IP Durable Object state. We document the pattern's invariants, the adversarial probes that verify them, and the empirical operating properties from a production deployment.
1. Introduction
Public unauthenticated APIs that surface multi-tenant marketplace data — bounty boards, agent directories, public catalogs of customer-facing content — face a compound security problem that no single primitive resolves cleanly. Each of the following must hold simultaneously:
- Pagination cursors must not enable enumeration of private records. A naive cursor (e.g., a row ID or a timestamp) lets an unauthenticated client iterate the table by incrementing the cursor, exposing rows the visibility predicate may not have intended to surface.
- Visibility must require consent from multiple parties. A bounty’s visibility on the public marketplace requires (a) the bounty’s tenant has opted into public marketplace visibility, and (b) the squad bidding on the bounty has opted into being publicly listed. Either party’s withdrawal of consent must immediately remove the relationship from public surfaces.
- Field projection must prevent private-data escape. A query that uses
SELECT *or a spread operator over the row’s columns will surface any column added in a later migration regardless of whether the column was intended for public exposure. - Rate limiting must prevent enumeration even at high cost. Even cryptographically-secure cursors do not prevent a determined attacker from making millions of requests; rate limiting must operate at the per-IP layer with fail-closed behavior on signal loss.
Each individual primitive is well-understood in isolation. HMAC-signed cursors are documented in API design literature. Consent flags and dual-consent predicates are standard in privacy-engineering practice. Whitelist projection is a known pattern in defense-in-depth column-level security. Rate limiting via per-IP state is the conventional approach. What is not documented is the compound deployment — how these primitives interact, what invariants hold across them, and what adversarial probes verify the compound pattern.
We describe the compound pattern deployed in production for a multi-tenant agent marketplace. The pattern has been verified through adversarial-parallel gating against named threat shapes. We document the invariants, the threat-shape probes, and the empirical operating properties.
2. The compound pattern
The pattern composes four layers, applied in order on every request:
flowchart TD REQ[Public unauthenticatedHTTP request] —> RL[Rate limit checkper-IP Durable Object] RL —>|allowed| HMAC[HMAC cursor verifyor null cursor for first page] RL —>|denied| R429[429 Rate Limited] HMAC —>|valid or null| WHITELIST[SQL query withenumerated SELECT list+ dual-consent predicate] HMAC —>|invalid| R400[400 Bad Cursor] WHITELIST —> PROJ[Whitelist projectionfield-by-field map] PROJ —> NEXT[Compute next cursorHMAC-sign the next-row anchor] NEXT —> RESP[Response withprojected rows + next cursor]
2.1 Rate limiting layer
Rate limiting executes first, before any cryptographic verification. The reasoning: cryptographic verification has measurable cost (HMAC computation per request); allowing unbounded requests to consume that cost is a denial-of-service vector in itself.
The rate limiter uses a per-IP Durable Object that maintains a token bucket per IP address. The IP address is read from the CF-Connecting-IP header only; X-Forwarded-For is not consulted because it is client-controllable and would allow trivial bypass.
The rate limiter is fail-closed. If the Durable Object is unreachable (network partition between the Worker and the DO service), the request is denied. This is the correct posture for unauthenticated public surfaces: better to return 429 to legitimate clients during a brief DO outage than to bypass rate limiting and serve attackers.
2.2 HMAC-signed cursor layer
Cursors are signed using HMAC-SHA256 via the Web Crypto API’s crypto.subtle.sign and crypto.subtle.verify methods. The Web Crypto API guarantees timing-safe verification by API contract; string-comparison-based MAC verification (e.g., comparing two hex strings with ===) is timing-vulnerable and is not used.
The cursor body encodes the next-row anchor (typically a sort-key value plus the row’s primary identifier within the visible result set). The HMAC is computed over the body using a server-only secret. The cursor is base64url-encoded for HTTP-safe transport.
The cursor body intentionally does not include the tenant’s primary key or any other private identifier. If the cursor is leaked or replayed, the only information exposed is the visible result set’s sort key, which is by construction information that the visibility predicate has already approved for public exposure.
2.3 SQL with dual-consent predicate
The query against the data layer uses an enumerated SELECT list (no SELECT *, no spread operator) and a WHERE clause that requires both consent flags to be true. For a bounty marketplace:
SELECT b.id, b.title, b.description, b.status, b.posted_at, t.slug AS tenant_slug, s.slug AS squad_slug
FROM bounties b
INNER JOIN tenants t ON t.id = b.tenant_id
LEFT JOIN squads s ON s.id = b.claimed_squad_id
WHERE t.marketplace_public_bounties = 1
AND b.status IN ('open', 'in_review')
AND (s.id IS NULL OR s.marketplace_visible = 1)
AND b.id > :cursor_anchor
ORDER BY b.id
LIMIT :page_sizeThe query enforces:
- Tenant-level consent via
t.marketplace_public_bounties = 1. The tenant has explicitly opted into public marketplace visibility for their bounties. - Squad-level consent via
s.marketplace_visible = 1. If a squad has claimed the bounty, the squad has opted into being publicly listed. A claim by a non-opted-in squad must not surface the bounty publicly. - Status whitelist via
b.status IN ('open', 'in_review'). Failed, rejected, or disputed bounties must not surface publicly even if the consent flags are true. The status whitelist is hardcoded; new statuses cannot be silently added by a downstream migration.
The cursor anchor is integrated into the WHERE clause as the lower bound for the next page.
2.4 Whitelist projection layer
After the SQL query returns, each row is projected through an explicit field map before being included in the response:
function projectBountyForPublic(row: BountyRow): PublicBounty {
return {
id: row.id,
title: row.title,
description: row.description,
status: row.status,
posted_at: row.posted_at,
tenant_slug: row.tenant_slug,
squad_slug: row.squad_slug ?? null,
}
}The projection is enumerated: each field is named explicitly. Spread operators are forbidden by the substrate’s gate function; an attempt to use { ...row, redacted_field: undefined } would bypass the projection and is caught at gate time.
The projection serves as a defense-in-depth layer against migration drift. If a future migration adds a private column to the bounties table (say, internal_priority_score), the projection’s enumerated form does not pick up the new column. The marketplace surface remains safe even before the gate function explicitly reviews the new migration’s interaction with the public surface.
3. Threat-shape verification
The pattern is verified against named threat shapes documented in Mumega 200.002 — Threat-Shape Vocabulary. The relevant probes:
flowchart LR G[Adversarial gate] —> P1[Probe: cursor enumeration] G —> P2[Probe: cross-tenant cursor replay] G —> P3[Probe: spread-leak via projection drift] G —> P4[Probe: rate-limit IP spoof] G —> P5[Probe: SQL injection via cursor decode] G —> P6[Probe: timing oracle on HMAC verify]
Each probe runs as a hermetic test in the substrate’s continuous gate suite. The probes have not failed in the operating window; the pattern’s invariants hold under adversarial verification.
4. The whitelist projection invariant
The whitelist projection layer is the load-bearing defense against the most subtle threat the public surface faces: migration drift. A new migration adds a column intended for internal use. The query layer is updated to use the new column for internal logic. The projection layer, if it uses SELECT * or spread operators, picks up the new column silently. The marketplace surface now exposes data that nobody intended to expose.
The substrate’s gate function enforces an explicit invariant: every public-surface route file must use enumerated projection. The gate runs a structural grep over the route file looking for spread operators in projection contexts; finding one fails the gate. A developer who wants to add a column to the marketplace surface must explicitly add it to the enumerated projection, which surfaces the change for adversarial review at gate time.
The invariant is named in the substrate’s threat-shape vocabulary as spread-leak via projection drift. Its instance count is currently zero; the invariant has held since the public surface was first deployed.
5. The rate-limiter fail-closed property
Rate limiting on public surfaces is conventionally treated as a soft guarantee — if the rate limiter is unavailable, requests proceed and the operator is alerted. We argue this posture is wrong for public unauthenticated surfaces.
A public surface that proceeds when the rate limiter is unavailable is exactly the case where a determined attacker can exploit a brief DO outage to enumerate the surface at scale. The conventional posture optimizes for legitimate-client availability during outages; the threat-aware posture optimizes for surface integrity during outages.
The substrate’s public surface is fail-closed. If the Durable Object is unreachable, the request is denied with HTTP 429. Legitimate clients see brief unavailability during outages; attackers see no exploitation window. The tradeoff is correct for the surface’s threat profile.
6. The CF-Connecting-IP-only invariant
The rate limiter reads the client’s IP address from the CF-Connecting-IP header exclusively. The X-Forwarded-For header is not consulted, even though many proxy chains populate it with what appears to be the original client IP.
The reason: X-Forwarded-For is client-controllable. An attacker can send arbitrary X-Forwarded-For values. If the rate limiter uses X-Forwarded-For, the attacker can bypass per-IP limits by varying the header on each request. CF-Connecting-IP is set by Cloudflare’s edge based on the actual TCP connection’s source address; it is not client-controllable.
The invariant is enforced at the gate layer: every public-surface route file is grepped for X-Forwarded-For references; finding one fails the gate.
7. Empirical operating data
The pattern is deployed in production for a multi-tenant agent marketplace surface. We report aggregate observations:
- The public surface is exposed at the substrate’s
/api/marketplace/*route family - Rate limiting operates at default thresholds (300 requests per minute per IP)
- HMAC cursors are signed with a per-deployment secret rotated on a per-quarter cadence
- The whitelist projection has been updated three times during the operating window, each time through explicit gate review
- No private-data escape incidents have been observed
- No cursor enumeration attempts have produced data outside the visibility predicate
- Rate-limit denials in the operating window are dominated by automated security scanners; legitimate-client denials are rare
8. Comparison to alternative approaches
We discuss why the compound pattern was selected over alternatives.
Cursor as encrypted blob. Encrypt the cursor body with a server-only key; decryption fails on tampering. Equivalent security to HMAC for this use case; HMAC was selected for slightly lower CPU cost and simpler migration if the secret rotates.
OAuth or session tokens for the public surface. Require all access to be authenticated; eliminate the unauthenticated case. Rejected because the public surface’s value proposition is unauthenticated discoverability; gating it behind authentication is functionally equivalent to not having a public surface.
Single-consent visibility (only the resource owner needs to opt in). Simpler to implement; rejected because it produces compound surface exposure: a tenant who opts in to public bounties can inadvertently expose squads that have not opted in to public listing. Dual-consent is the correct discipline for compound resources.
SELECT * with a redact-list of forbidden fields. Rejected as exactly inverse to the safety invariant: a redact-list captures “fields we know are sensitive”; the threat is fields we have not yet considered. Whitelist projection is allow-list based and therefore safe by default.
Rate limiter with X-Forwarded-For trust. Rejected as enabling trivial header-rotation bypass.
9. Forward work
HMAC secret rotation discipline. The cursor secret is rotated quarterly; cursors signed with the prior secret remain valid for one rotation cycle to avoid breaking in-flight pagination. Forward work includes formal verification that the rotation overlap window cannot be exploited (e.g., by replaying a rotation-overlap cursor against a different surface).
Per-tenant rate-limit configuration. Tenants with high-traffic public surfaces may need higher per-IP rate limits than the default. Forward work includes a tenant-configurable rate limit with substrate-side floor (no tenant can disable rate limiting entirely).
Cross-region consistency for the Durable Object. The current rate limiter operates in a single region per IP. Forward work includes cross-region replication for global rate limits.
Cursor compression for large-page-size workloads. The current cursor format scales linearly in the sort-key field count; compact binary encoding could reduce cursor size for workloads with multi-field sort keys.
10. Conclusion
We describe a compound pattern for public unauthenticated multi-tenant marketplace APIs: HMAC-signed cursors via the Web Crypto API for timing-safety, dual-consent visibility predicates evaluated at SQL read time, enumerated whitelist projection forbidding spread operators, and per-IP rate limiting with fail-closed behavior on Durable Object unavailability. The pattern composes individual primitives whose combination has not been documented in the literature; we report it here with the adversarial verification record from a production deployment.
The pattern is appropriate for any multi-tenant orchestration substrate that needs to expose a public API surface without compromising private-data isolation. The whitelist projection invariant is the load-bearing defense against migration drift; the fail-closed rate-limiter posture is the correct threat-aware tradeoff for public surfaces; the HMAC cursor with body that excludes private identifiers prevents enumeration even under cursor leakage.
The reference implementation is open-source preparation under AGPL-3.0. Companion to Mumega 200.002 — Threat-Shape Vocabulary for the named threats this pattern verifies against.