Skip to content

feat(storage): service-wide, stackable guard layer for API limits (#1630) - #1925

Open
AkramBitar wants to merge 2 commits into
mainfrom
feature/1630-api-payload-and-query-bounds
Open

feat(storage): service-wide, stackable guard layer for API limits (#1630)#1925
AkramBitar wants to merge 2 commits into
mainfrom
feature/1630-api-payload-and-query-bounds

Conversation

@AkramBitar

@AkramBitar AkramBitar commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Add size limits to the storage service so one huge write or one unlimited query can't overload the database. Fixes the [MED] issue #1630 (denial of service from unlimited resource use, CWE-400 / CWE-770).

What changed since the first revision

The first revision implemented the limits only for the transaction store, via per-constructor WithMax* options + inline checks. Per review feedback (that approach doesn't scale beyond one store), this has been reworked into a single, stackable guard decorator layer applied across the whole storage service. The transaction store was migrated onto it, so there is now one mechanism.

How it works

  • New package token/services/storage/db/guard/: a Policy (loaded once from config), payload-size checks, a row-capping LimitIterator, and interface-embedding decorators for the transaction, token, endorser, identity and wallet stores. Each decorator embeds the store interface (delegating everything) and overrides only the methods that need a check, plus the nested write-transactions and returned iterators.
  • Applied once at the multiplexed.Driver seam: every NewXxx result is wrapped, so all backing drivers (SQLite, PostgreSQL) and all stores are covered uniformly. Additional layers (metrics/tracing) can stack the same way, with the concrete SQL store at the bottom.

What is guarded

Write payload sizemaxPayloadSize (default 4 MiB; 0 disables), rejected before reaching the DB:

  • transaction: AddTokenRequest, AddTransaction, AddMovement, AddTransactionEndorsementAck
  • token: StoreToken, StorePublicParams, StoreCertifications
  • endorser: AddValidationRecord
  • identity: StoreIdentityData, StoreSignerInfo, RegisterIdentityDescriptor, AddConfiguration
  • wallet: StoreIdentity

Read capsmaxPageSize (default 1000):

  • paginated QueryTransactionsnil / pagination.None() / over-max pages are rejected
  • streaming iterators — token unspent/spendable/unsupported iterators, QueryValidations, QueryTokenRequests, IteratorConfigurations — capped by a LimitIterator that errors (rather than silently truncating) once the cap is exceeded

Intentionally uncapped: QueryMovements (feeds balance totals; dropping rows would silently return wrong balances).

Also closes two gaps in pagination.ValidateLimited: keyset page size is now capped against maxPageSize, and a typed-nil offset is guarded.

Configuration / upgrade note

Limits are on by default (4 MiB / 1000), so existing deployments get them automatically. Override via token.storage.maxPayloadSize / token.storage.maxPageSize; an explicit 0 disables the respective check. Paging on QueryTransactions cannot be turned off — nil/None are rejected — so any caller that read everything at once now pages (see collectAllTransactions / checks.go).

Not yet addressed (tracked follow-up)

These vectors can't be closed by a wrapper alone, because the SQL has already materialised the full result before the decorator sees it — they need query-level changes and will be handled in a follow-up:

  • Materialized slice/map reads: ListUnspentTokens, ListUnspentTokensByWallets, ListHistoryIssuedTokens, ListAuditTokens, QueryTokenDetails, ConfigurationsByID, GetWalletIDs, GetExistingSignerInfo — need a SQL-level LIMIT (with a defined order + documented truncation) or conversion to iterators.
  • Variadic input-size caps: DeleteTokens, GetTokens, GetTokenRequests — a huge id list expands into a huge IN (...) clause.
  • Keystore.Put: opaque value serialised inside the store, so its size is only known at marshal time.

Closes #1630.

@AkramBitar
AkramBitar marked this pull request as draft July 15, 2026 14:06
@AkramBitar AkramBitar self-assigned this Jul 15, 2026
@AkramBitar AkramBitar added this to the Q3/26 milestone Jul 15, 2026
@AkramBitar AkramBitar changed the title feat(storage): enforce API payload size limits and query result caps (#1630) Enforce API payload size limits and query result caps (#1630) Jul 15, 2026
@AkramBitar
AkramBitar force-pushed the feature/1630-api-payload-and-query-bounds branch from a177bc5 to 2a4b101 Compare July 16, 2026 14:18
@AkramBitar
AkramBitar requested review from HayimShaul and adecaro July 16, 2026 14:19
@AkramBitar
AkramBitar marked this pull request as ready for review July 16, 2026 14:32
@adecaro

adecaro commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Hi @AkramBitar , thanks for submitting this.

My concern is the following: The storage service has many queries that would require similar constraints. Right now, this opts-based approach works only for the Transactions db.

I would suggest to perform an analysis of the entire storage service to understand which limits need to be put in place and then design a flexible solution to address the issues. A wrapper-based solution could also be interested so that we can stack different layers on top of each other each perform a different function and the bottom one running the actual SQL query.

What do you think?

Thanks 🙏

@AkramBitar
AkramBitar force-pushed the feature/1630-api-payload-and-query-bounds branch from d3b4171 to 5b0249d Compare July 21, 2026 14:59
@AkramBitar AkramBitar changed the title Enforce API payload size limits and query result caps (#1630) feat(storage): service-wide, stackable guard layer for API limits (#1630) Jul 21, 2026
@AkramBitar
AkramBitar marked this pull request as draft July 21, 2026 15:37
@AkramBitar
AkramBitar force-pushed the feature/1630-api-payload-and-query-bounds branch 3 times, most recently from ba65cd2 to 364565e Compare July 23, 2026 12:02
@AkramBitar

Copy link
Copy Markdown
Contributor Author

Hi @AkramBitar , thanks for submitting this.

My concern is the following: The storage service has many queries that would require similar constraints. Right now, this opts-based approach works only for the Transactions db.

I would suggest to perform an analysis of the entire storage service to understand which limits need to be put in place and then design a flexible solution to address the issues. A wrapper-based solution could also be interested so that we can stack different layers on top of each other each perform a different function and the bottom one running the actual SQL query.

What do you think?

Thanks 🙏

Hi @adecaro,

Thanks a lot for the review. Good idea. I implemented it.

Thanks,
Akram

@AkramBitar
AkramBitar marked this pull request as ready for review July 23, 2026 12:03
Comment thread token/services/storage/db/common/checks.go
Comment thread token/services/storage/db/guard/token.go Outdated
Comment thread token/services/storage/db/sql/query/pagination/validate.go
Comment thread token/services/storage/db/guard/transactions.go Outdated
@AkramBitar

Copy link
Copy Markdown
Contributor Author

@adecaro thanks a lot for the comments.
I have answered all your comments. Please let me know if you have additional comments.

@AkramBitar
AkramBitar force-pushed the feature/1630-api-payload-and-query-bounds branch from 3ea0527 to 6fb9045 Compare July 29, 2026 11:09
@AkramBitar
AkramBitar force-pushed the feature/1630-api-payload-and-query-bounds branch from 6fb9045 to 97de92f Compare August 18, 2026 08:14
The guard layer applied LimitIterator to QueryValidations,
QueryTokenRequests and IteratorConfigurations. None of those three
accepts a pagination argument, so the error they raised once the cap was
exceeded ("must paginate") asked for something the API cannot do.

IteratorConfigurations made this a functional regression rather than
just misleading advice: LocalMembership.storedIdentityConfigurations
drains it in full via collections.ReadAll on the identity Load path, so
a node holding more than maxPageSize (default 1000) stored identity
configurations of one type failed to load identities, recoverable only
by raising or disabling the limit globally.

Remove the cap from all three and record at each site why the read is
not bounded here, matching the reasoning already applied to the
token-store iterators. Row-capping these reads needs a SQL-level LIMIT
or paging in their signatures, which the docs now track as follow-up.
LimitIterator has no remaining callers, so it and its tests are removed
rather than left as dead code.

maxPageSize continues to bound QueryTransactions on the owner and audit
transaction stores, which does take a pagination argument, so rejecting
an unbounded page there is actionable. Write payload limits are
unchanged.

Also correct the docs, which claimed the token store's
unspent/spendable/unsupported iterators were row-capped while
guard/token.go explicitly did not cap them and overrode no read method.

Signed-off-by: AkramBitar <akram@il.ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API-level payload size limits and query result caps [MED]

2 participants