Guide 088 worked guide

How to guard against cross-tenant data in batch operations

Review batch operations for cross-tenant data by checking every item, duplicate handling, metadata resolution failures, retries, and when outputs become available.

Treat every member of a batch as its own authorization decision. Resolve the caller's tenant and action from trusted identity, load each requested object's authoritative parent or ownership record, and reject or omit any member that does not belong to the authorized scope before reading or changing protected content. Bind the storage operation to that scope as a second boundary. For a write that must succeed as one business action, reject the entire mixed or uncertain request and use an atomic operation where available. For a read that intentionally returns a permitted subset, release only members with a positive decision and state whether the result is partial. The answer changes with the API contract, but a single permitted ID must never authorize the whole batch. OWASP makes that per-item rule explicit in its authorization decision and output guidance.

Batch APIs are easy to get wrong because there are several sets: the client's requested IDs, the IDs selected for a database call, the records actually returned, retry keys, and the final response. Authorization must hold across all of them. A tenant filter in an initial query cannot protect a later BatchGet built from caller-supplied IDs. An IAM policy can constrain partition keys, but it cannot decide whether an ownership field inside an allowed record is truthful. Keep the application and storage checks independent.

Illustrative case: three invoice IDs in one request

Illustrative case; the organization, IDs and responses are invented. Alder Ledger lets account A download invoice summaries in one request. Its endpoint accepts up to 20 invoice IDs. The signed-in caller is an account-A billing analyst. The request contains A-17, B-44 and A-22. A-17 belongs to account A. B-44 belongs to account B. A-22 appears under an A-prefixed lookup key, but its persisted parent reference points to a different account because of a bad migration.

The old handler checks that the caller has invoice:read in A, calls BatchGet with all three IDs and returns whatever comes back. The role check is real, yet it says nothing about whether any particular invoice belongs to A. The mismatch on A-22 is a second reason to withhold it: a convenient key prefix cannot overrule conflicting ownership evidence.

Member Key or selector says Persisted owner chain says Decision before content release
A-17 Account A Account A Permit if current invoice-read grant also holds
B-44 Account B Account B Deny for this caller
A-22 Account A Different account Withhold and investigate inconsistent state

For a download API whose documented result is “all requested invoices or an error,” return no summaries and a generic failure for this mixed request. For an API explicitly designed to return permitted items only, A-17 may be returned, but the response must never contain the foreign or inconsistent records, and the caller must not learn their titles or ownership from error details. Decide this behavior in the contract; do not silently choose it inside a catch block.

Track sets through the storage call

The mixed request is not ready for a content read. Authoritative metadata gives:

requested = {A-17, B-44, A-22}
permitted = {A-17}
denied = {B-44}
inconsistent = {A-22}

For Alder's chosen all-or-error contract, the final summary set is empty and no content batch is sent. For an explicitly different permitted-subset endpoint, only A-17 becomes a content key. The external response must not explain that B-44 belongs to another named customer or include A-22's contradictory parent. Internally, preserve those distinctions for repair.

Now consider a clean request for three permitted A invoices: A-17, A-18, A-19. The storage response arrives out of order and is incomplete:

submitted:      [A-17, A-18, A-19]
returned:       [record(A-19), record(A-17)]
unprocessed:    [A-18]

Pairing records with decisions by array position would attach A-19 to A-17's permit. Match by the complete storage key and compare each returned record's persisted owner chain with its own decision. AWS documents both unordered BatchGetItem results and unprocessed keys. Include identifying keys in the projection so reconciliation is possible.

A concrete internal reconciliation result is:

A-17: returned, owner matches, held for response
A-18: unprocessed, no conclusion about existence or ownership
A-19: returned, owner matches, held for response

“Held” matters for all-or-error behavior: do not stream the first two summaries and later claim the whole operation failed without disclosure. Bound how much can be buffered and how long the operation may continue. Under permitted-subset semantics, a partial response can release the authorized completed items, provided its contract accurately represents incompleteness without leaking forbidden details.

A retry is another protected operation

Before retrying A-18, the caller's invoice grant is removed. The original decision is no longer sufficient under Alder's current-grant policy. Re-establish authority, discover the removal, and do not submit another content read. The final all-or-error response releases no summaries, including those held from the first attempt. An automatic SDK retry that bypasses this application boundary deserves explicit review; “retry only unprocessed keys” preserves item identity but not permission freshness.

If authority remains current, retry only A-18 with the same tenant-bound credential and resolved key. Apply the same owner comparison to its returned record. A missing item is distinct from an unprocessed one: the former was not returned as an existing record by that read; the latter was not completed. BatchGetItem uses eventually consistent reads by default, so absence alone cannot prove an item was deleted or never existed. Strongly consistent item reads still do not make the whole batch a transactional snapshot. Neither may be replaced by a broad fallback lookup.

This is the batch invariant in compact form:

released keys ⊆ returned-and-reconciled keys ⊆ permitted keys ⊆ requested keys

It is a set relationship, not a substitute for time-aware authorization. A key can leave the permitted set after an earlier read.

Writes need a different business decision

Suppose the action is “mark these three invoices paid together.” Partial success could leave the ledger inconsistent. Per-item authorization is necessary, but an ordinary batch write is not enough to provide all-or-nothing effects. AWS states that BatchWriteItem is not atomic as a whole and does not support per-item conditions. Use a suitable transactional design that preserves the business invariant and revalidates the required state, or redesign the API to expose independent operations honestly.

An IAM partition restriction can independently block a foreign key. For multivalued context, inspect how every key is constrained and whether absent keys fail closed; AWS documents the ForAllValues missing-context behavior. Such a policy cannot resolve A-22's inconsistent parent inside an allowed partition. Keep the content withheld until an authorized integrity repair establishes the correct relationship.

All sets and events above are illustrative, not observations from a running service. For an implementation review, the decisive artifacts are the key builder, permit-to-record mapping, retry wrapper, publication behavior and actual credential policy. Those expose failures that a single “batch access denied” screenshot cannot.

Where a second boundary helps—and where it stops

In a shared table, an IAM-visible partition restriction can prevent an ordinary data credential from reading another tenant's partition even if application code accidentally passes B-44. That is useful defense in depth, provided every relevant batch and transaction action uses the constrained credential and no ambient fallback exists. It does not prove that every item in an allowed partition has the correct parent, section or viewer. It also cannot by itself protect a broad secondary index, object store export or cached report. The addressed service must validate persisted ownership and output scope.

Ask for evidence at both layers. A source review can show the intended checks and key construction. A controlled A/B-tenant operation in the actual target environment can show whether the application and independently scoped storage role deny foreign keys. A policy simulator or successful local request is narrower evidence. Do not claim zero cross-tenant risk from one passing case.

Handle disagreement and incomplete work openly

If Alder Ledger's A-22 migration conflict is genuine, quarantine the item and repair its parent relationship through an authorized data process; do not relabel it account A merely so the batch passes. If a reviewer finds a legitimate shared-account invoice rule, update the policy and the ownership evidence before allowing it. If BatchGet returns an unprocessed key, do not turn that absence into a deny verdict about ownership or a claim that the invoice does not exist. Separate denied, missing, inconsistent and not processed outcomes internally, while keeping the external error suitably restrained.

Normalize the batch before making security decisions

Alder's public contract should define what repeated identifiers mean. A request for [A-17, A-17, A-18] might be normalized to two unique reads, or it might preserve caller order in the response. Either behavior can be reasonable for a read API if documented. What matters is that duplicate positions do not manufacture additional authority or make reconciliation confuse one object's decision with another's.

Keep the original request mapping separately from the unique resolved keys. Authorize each unique target under the intended action, reconcile storage results by full key, and build the response according to the documented order and duplicate policy. Do not infer the owner of a later result from the array position of the earlier permit. The unordered-response case already shows why positional trust fails.

For writes, duplicate references deserve an explicit business rule. “Pay invoice A-17 twice” should not accidentally become two ledger changes merely because the input array repeats an ID. The authorization review should identify the intended operation identity and route that question to the transaction owner. Per-item authorization and business deduplication solve different problems; both may be needed for the chosen batch action.

Preserve the contract when a dependency fails

Suppose the metadata lookup resolves two items and times out on the third. Under the all-or-error contract, the handler has not established permission for the complete request. It should not treat the unknown item as absent and proceed with a success response. Under a permitted-subset contract, the response may contain completed authorized items only if the API clearly represents partial work and gives the client a safe way to continue.

Also consider a failure after content has been fetched but before the response is sent. Internally, the service may have held authorized summaries in memory. That is different from releasing them to the caller, and the record should distinguish those stages. A retry must not blindly reuse the earlier permission decision if the application's stated freshness policy requires a new one.

These branches are useful acceptance cases because they exercise the implementation's bookkeeping, not just the obvious foreign ID. The final review packet should show how duplicates, unresolved metadata, unprocessed storage keys, and revoked permissions affect the same requested-to-released set relationship. A batch is safe only to the extent that its actual response construction preserves the intended item decisions.

Next action: choose one batch read and one batch write in your service. Draw their requested, permitted, stored and returned ID sets, then run an authorized mixed-tenant fixture through both the application decision and the storage credential. For single-object ownership, continue with parent and child resource authorization; for attacker-controlled selectors, read tenant ID spoofing in APIs.