Field Note 10 current
Pin the expected owner on cross-account resource calls (confused-deputy defense)
TL;DR
Holding permission to perform an operation is not proof that the resource you’re operating on is the one you meant. A confused deputy is your own workload, using its legitimate credentials, tricked into reading from or writing to a resource owned by someone else — because a bucket name, ARN, account ID, or key reference reached it from untrusted input, stale config, or another service and pointed at an attacker-controlled target.
The rule: every authorized call that could cross an account or ownership boundary must assert the expected owner, in depth:
- Pass the owner-assertion parameter the API provides —
ExpectedBucketOwner(andExpectedSourceBucketOwner/ExpectedDestinationBucketOwneron copy) for S3, the equivalent owner/account/registry parameter elsewhere — so the provider rejects the call if the target isn’t owned by the account you expect. - Bound resources with IAM condition keys —
aws:ResourceAccountand/oraws:ResourceOrgID(and GCP VPC Service Controls / project pinning) — so even an unasserted call can’t reach outside your own org. - Validate untrusted identifiers before use — any bucket name, ARN, or resource path that came from request input, another service, or config is checked (account, partition, region, allowlist) before a call is issued; never pass an attacker-influenced ARN straight through.
- Pin inbound cross-service trust — role trust policies that let a cloud service act on your
behalf set
aws:SourceArn/aws:SourceAccount(andsts:ExternalIdfor third-party role assumption) so the deputy can’t be invoked on a forged source.
If the API genuinely offers no owner-assertion mechanism and no condition key applies, that’s a carve-out — document it at the use site and compensate (see Carve-out).
Context
Federated, least-privilege workload identity (ZFN-9) makes workloads good at authenticating — proving who they are and getting scoped, short-lived credentials. But authentication and authorization answer “may this workload perform this action?” They do not answer “is the resource this action targets the one we intended?” That second question is where the confused-deputy class of bug lives, and it’s invisible to the credential model: the call is perfectly authorized; it just lands on the wrong owner’s resource.
The canonical shape: a workload accepts a bucket name (or object ARN, KMS key id, queue URL) as input — from an API request, a webhook payload, a row written by another tenant’s flow, or a config value — and issues an S3/KMS/SQS call against it using its own credentials. If an attacker can influence that identifier, they can point your deputy at a resource in their account. Depending on the operation that means: you write data you believe is private into a bucket the attacker can read; you read attacker-supplied content and treat it as trusted; you’re billed for and logged as the actor on their resource; or you leak the fact and content of the operation across an account boundary. Cloud providers call this out explicitly and ship defenses precisely because it’s easy to get wrong.
There’s a mirror-image, inbound version: services that assume one of your roles to act on your
behalf (object-storage → function triggers, pub/sub → a role, audit/config services). If the role’s
trust policy only says “service X may assume me,” a different customer can configure their
resource in service X to assume your role — the cross-service confused deputy. The fix is to pin
the trust to a specific source (aws:SourceArn / aws:SourceAccount), and for third-party
integrations to require a secret sts:ExternalId.
None of this is exotic, but it’s the kind of defense that’s reliably forgotten because the happy path works without it — the call succeeds, tests pass, and the missing owner check only matters when an attacker supplies a hostile identifier. A security-first ordering (ZFN-2) says pay for it up front.
Recommendation
When a workload makes an authorized call to a cloud resource that could cross an account or ownership boundary, assert the expected owner — defense in depth across the request, the policy, and the code. Concretely:
- Use the API’s owner-assertion parameter wherever one exists. For S3 this is
ExpectedBucketOwneron every operation that accepts it, andExpectedSourceBucketOwner/ExpectedDestinationBucketOwneronCopyObject. For other services, use the analogous owner/account/registry scoping parameter (e.g. ECRregistryId, account-qualified identifiers). The expected owner is a configured, trusted value — your own account id(s) — never derived from the same untrusted input that supplied the resource name. - Bound resources and sources with IAM condition keys. Policies attached to your roles use
aws:ResourceAccountand/oraws:ResourceOrgIDto confine actions to resources your accounts/org own, andaws:SourceAccount/aws:SourceArnon resource and trust policies for cross-service calls. This is the backstop that holds even when an application-level assertion is missed. On GCP, the equivalent guardrails are VPC Service Controls perimeters and explicit project pinning. - Validate untrusted resource identifiers before the call. Any bucket name, ARN, key id, queue URL, or resource path that originates outside the calling service is parsed and checked (partition, account, region, and an allowlist of permitted owners/names) before a call is issued. Don’t forward an attacker-influenceable ARN into an SDK call unchecked, even when a condition key would also catch it.
- Pin inbound cross-service trust. Role trust policies that permit a cloud service to assume the
role constrain it with
aws:SourceArn/aws:SourceAccount; third-party cross-account role assumption requires a non-guessablests:ExternalId. A bare “service may assume me” trust policy is a defect.
Carve-out
Some APIs offer no owner-assertion parameter, and some call sites can’t be covered by a resource-account condition key (e.g. a deliberately cross-account integration). Where that’s genuinely the case, it’s a carve-out, following the documented-at-the-use-site discipline in ZFN-3:
- Documented at the use site. A comment where the call is made states that no owner-assertion mechanism exists for this API/operation, what compensating control is in place instead, and links this note.
- Compensated. A concrete alternative defense is applied and named: code-level validation of the resource’s account/owner against an allowlist, a scoping resource policy, a VPC-SC perimeter, or a network boundary. “No mechanism available” is never the same as “no check.”
- Minimally scoped and reviewed. The cross-owner reach is the least the integration needs.
A cross-account-capable call with neither an owner assertion nor a documented compensating control is a defect, not an exception.
Consequences
Easier:
- A whole class of cross-account data-exfiltration and data-poisoning bugs is closed by default, including the ones only an attacker-supplied identifier would ever trigger.
- The defense is layered, so a single missed application-level assertion is still caught by an org/account condition key.
- Audit improves: owner assertions and
aws:ResourceAccount/SourceArnconditions make “this workload only ever touches our own resources” an enforceable, reviewable property. - Inbound trust pinning closes the cross-service confused-deputy hole bare service-trust policies leave open.
Harder:
- More to remember at every call site — threading an expected-owner value and validating untrusted identifiers. Helper wrappers reduce but don’t remove the burden.
- Condition keys can over-constrain:
aws:ResourceAccount/aws:ResourceOrgIDwill break intended cross-account access unless explicitly carved out — which is the point, but it means legitimate cross-account flows must be surfaced and allowlisted. - Genuinely cross-account integrations carry carve-out obligations rather than being free.
- Expected-owner values are configuration (your account ids); multi-account / multi-region topologies make “what is our account here?” something to wire correctly.
New obligations:
- New code that calls a cloud resource whose identity can be externally influenced asserts the expected owner (parameter where available; otherwise the carve-out). Reviewers treat a missing assertion like a missing authentication check.
- Role definitions default to bounding resources with
aws:ResourceAccount/aws:ResourceOrgID(or GCP VPC-SC), and trust policies pinaws:SourceArn/aws:SourceAccount/sts:ExternalId. - Untrusted resource identifiers are validated before use; passing one through unchecked is a defect.
References
- ZFN-9 — how a workload authenticates to a cloud; this note verifies the target of an authorized call is owner-verified. A call uses both.
- ZFN-3 — the carve-out-with-documentation-at-the-use-site pattern reused here.
- ZFN-2 — security-first ordering that justifies the per-call-site cost.
- AWS: the confused deputy problem &
aws:SourceArn/aws:SourceAccount; S3ExpectedBucketOwner; IAMaws:ResourceAccount/aws:ResourceOrgID. - GCP: VPC Service Controls for an ownership/perimeter equivalent.
Changelog
- 2026-06-12: First published as a Field Note.