← Back to Attack Research

Signature-valid is not authorized-for-this-resource

An agent verifies a JWT's signature and proceeds. The signature is genuine. But it never checks that the token's source_id claim matches the resource the caller asked for, so a tenant holding a perfectly valid token of its own reads and rewrites another tenant's object. Here is the JWT claim-binding decision tree, grounded in CVE-2026-53471 against the kubev2v migration-planner, and the contract that ends the class.

An attacker is a legitimate tenant of your platform. They authenticate normally and your service hands them a real, properly signed JWT scoped to their own source. They do not forge anything. They do not break the signature. They simply call an endpoint with their own valid token and point it at a resource id that belongs to a different tenant. The server checks that the signature is valid, sees that it is, and runs the operation, because nothing in the handler compares the token's source_id claim to the resource being touched. This is the confused deputy with a real token. CVE-2026-53471 (CVSS 9.6, assigned by Red Hat) is exactly this shape in the kubev2v migration-planner: the UpdateSourceInventory and UpdateAgentStatus handlers validate the token's signature but never validate the source_id claim inside it against the requested source id, so an authenticated user manipulates data across tenant boundaries. The flaw is CWE-639, Authorization Bypass Through User-Controlled Key. This piece walks the decision tree from a valid token to a cross-tenant write, and the contract that closes it.

There is a deep and common confusion in how teams reason about JSON Web Tokens. A valid signature feels like the end of the security question. The token was issued by us, it has not been tampered with, the math checks out, so the caller is trusted. But a signature answers exactly one question: was this token minted by the issuer and left unmodified. It does not answer the question that actually matters at the resource boundary: is the subject of this token allowed to act on this specific object. Those are different checks. Signature verification is authentication of the token. Claim-to-resource binding is authorization for the request. When a service ships the first and forgets the second, every holder of any valid token becomes a holder of everyone's data.

This is part of our attack-research series on the classes that ship despite a decade of patches. It is a close cousin of our piece on cross-tenant BOLA in operations tooling, and worth stating plainly: same outcome, different root cause. There the server had no valid-token requirement to lean on at all and skipped the ownership filter in the query (a missing SQL WHERE user_group = ?); here the server has a cryptographically perfect token and still fails to bind that token's claim to the requested resource. One is a missing query filter. This one is a missing claim-binding check. Both end at the same place: a cross-tenant read or write. Verifiable security.

The attack pattern in one paragraph

A multi-tenant service issues a JWT to each tenant or agent, and the token carries a claim that scopes it: a source_id, a tenant, an org, an account. The token is signed by the issuer, so it cannot be forged or modified without detection. The caller then makes a request that names a resource: POST /sources/{id}/inventory, PUT /agents/{id}/status, or a body field carrying the target id. The correct design verifies two independent things on every request: that the signature is valid, and that the token's scoping claim matches the resource named in the request, that the source_id in the token equals the {id} in the path. The broken design verifies only the first. It confirms the signature, extracts the claims, and then operates on whatever resource id the request supplied, never comparing it to the claim the token actually carries. The attacker, holding their own entirely valid token, changes the resource id in the request to one belonging to another tenant. The signature still validates, because they never touched it. The operation runs against the other tenant's object. This is CWE-639, Authorization Bypass Through User-Controlled Key: the user controls the key that selects the object, and the server never confirms the key is theirs.

The unifying observation: a valid signature proves the token is real; a claim-to-resource check proves the token is for this resource. Services routinely ship the first and forget the second, on exactly the objects that hold cross-tenant data.

Why this still ships in 2026

Claim-confusion ships precisely because the failing check is invisible and the working check is loud. Four structural reasons, all visible in the CVE-2026-53471 shape.

  1. A valid signature masquerades as authorization. The signature check is the dramatic, satisfying part of token handling: keys, algorithms, a clear pass or fail. It produces a strong feeling of "verified." The claim-binding check is one unremarkable comparison that nobody notices is missing, because when it is absent, every legitimate request still works perfectly. The day-to-day behavior of the endpoint is identical whether the binding check exists or not. It only matters when the caller names a resource that is not theirs, and legitimate callers never do.
  2. The claim is trusted as identity but not enforced as scope. Once a service verifies the signature, it tends to treat every claim in the payload as gospel: the sub is who you are, the role is what you may do. But a scoping claim like source_id is not just a fact about the caller; it is a constraint on which resources the caller may touch. Reading the claim as identity ("this token belongs to source 42") without enforcing it as a constraint ("therefore this token may only act on source 42") is the entire gap. The migration-planner handlers read the token, accepted it, and then operated on the requested source without asserting the two were the same.
  3. Agent and machine-to-machine APIs feel pre-trusted. CVE-2026-53471 lives in an agent-API middleware, the path an autonomous agent uses to report inventory and status back to the planner. Internal agent traffic feels trusted by default: it is our agent, holding a token we issued, talking to our control plane. That framing quietly drops the per-resource authorization that a user-facing API would never skip. Machine identities still act across a tenant boundary, and a token scoped to one source must not be honored for another just because the caller is a daemon.
  4. The standard says to do this, as best practice, not as protocol. RFC 8725, the JSON Web Token Best Current Practices, is explicit that applications must validate the audience and the subject-issuer pairing and must reject tokens whose claims do not correspond to a valid subject at the application. But that is best-current-practice guidance layered on top of the base JWT specification, which mandates none of it at the verifier. A library validates the signature for you. It cannot validate that source_id matches your URL, because only your handler knows what resource the request targeted. That check is yours to write, and it is the one most easily left out.

The attacker decision tree

ATTACKER DECISION TREE JWT Claim Confusion, Tenant Collapse ┌───────────────────────────────────┐ │ 1. Obtain a VALID token for your own │ │ tenant. No forgery, no alg trick. │ │ - authenticate normally as source 42 │ │ - token.source_id = 42, signature good │ └─────────────────┌─────────────────┐ │ "the signature is valid, so attack │ the claim-binding that is missing" │ ▼ ┌────────────────────────────────────┐ │ 2. Find a request that names a RESOURCE │ │ id separately from the token │ │ - PUT /sources/{id}/inventory │ │ - PUT /agents/{id}/status │ │ - body field: {"source_id": } │ └─────────────────┌─────────────────┐ │ ▼ ┌────────────────────────────────────┐ │ 3. Change the resource id to one you do │ │ NOT own. Keep YOUR valid token. │ │ token.source_id = 42 (unchanged) │ │ request path id = 7 (victim) │ └─────────────────┌─────────────────┐ │ ▼ ┌────────────────────────────────────┐ │ 4. Server checks SIGNATURE, not BINDING │ │ - signature valid -> proceed │ │ - source_id 42 != path 7, never compared │ │ - one cross-tenant write = proof │ └─────────────────┌─────────────────┐ │ ▼ ┌────────────────────────────────────┐ │ 5. Iterate resource ids at tenant scale │ │ -> every tenant's object readable/owned │ └────────────────────────────────────┘

The five-step tree from a perfectly valid single-tenant token to a cross-tenant write, driven entirely by changing the resource id while keeping the same real token.

The decisive insight at step 1 is that the attacker never attacks the token. They do not forge it, strip its signature, or play algorithm tricks. They hold a genuine, issuer-signed token for their own tenant, and they keep it exactly as issued. The whole exploit is the gap between "this token is real" and "this token is for the resource you just named." Because the signature stays valid, every signature-based defense, every library check, every key-rotation control, passes cleanly while the cross-tenant write goes through underneath them.

A composite real-world scenario

The setting is a multi-tenant migration-planning platform. Each customer registers their source environment, a VMware estate they intend to migrate, and is assigned a numeric source_id. A lightweight agent runs inside each customer's network, authenticates to the platform's agent API, and is issued a signed JWT carrying source_id as a claim. The agent periodically reports discovered inventory and its own health back to the planner through UpdateSourceInventory and UpdateAgentStatus endpoints. The token signing is textbook: a strong key, a sane algorithm, short expiry. A security review of the auth layer returns green, because the signature pipeline is genuinely sound.

An attacker registers as a legitimate customer, gets assigned source_id 42, deploys the agent, and authenticates. They now hold a real token. They watch the request their own agent sends to report inventory.

# The attacker's own, legitimate agent request. source_id 42 is theirs.
# The token's source_id claim is also 42. Everything matches.
$ curl -s -X PUT https://planner.example/api/v1/sources/42/inventory \
    -H "Authorization: Bearer $VALID_TOKEN_SOURCE_42" \
    -H "Content-Type: application/json" \
    -d '{"vms": [ ... discovered inventory ... ]}'
{ "status": "updated", "source_id": 42 }

Then they change the resource id in the path to a source that is not theirs, and send the exact same, still-valid token. They do not modify the token at all. They guess source_id 7 belongs to another customer; the ids are small sequential integers, so enumeration is trivial.

# Same valid token (still source_id 42). Different resource in the PATH.
# Per CVE-2026-53471, the handler validates the signature but never
# compares the token's source_id claim to the requested source id.
$ curl -s -X PUT https://planner.example/api/v1/sources/7/inventory \
    -H "Authorization: Bearer $VALID_TOKEN_SOURCE_42" \
    -H "Content-Type: application/json" \
    -d '{"vms": [ {"name": "attacker-implant", "credentials": "..."} ]}'
{ "status": "updated", "source_id": 7 }     # <-- cross-tenant WRITE accepted

The server accepted it. As CVE-2026-53471 describes, the UpdateSourceInventory handler verified the bearer token's signature, found it valid, extracted the claims, and then operated on source 7 because that is what the path named, never asserting that the token's source_id claim (42) equaled the requested source (7). The same gap is present on UpdateAgentStatus, so the attacker can also rewrite another tenant's agent health, mask a failing migration, or inject false status. The NVD record notes the downstream impact directly: unauthorized inventory modification, credential injection, and assessment corruption across tenants.

# The same trick on agent status. Still the source_id 42 token.
$ curl -s -X PUT https://planner.example/api/v1/agents/7/status \
    -H "Authorization: Bearer $VALID_TOKEN_SOURCE_42" \
    -d '{"state": "healthy", "last_seen": "2026-06-11T09:00:00Z"}'
{ "status": "updated" }     # <-- another tenant's agent status, rewritten

No token was forged, no signature was broken, and no algorithm was confused. The attacker held one real token for their own tenant and changed the resource id in the request. The boundary that failed was never the signature. It was the missing comparison between the token's scoping claim and the resource the request actually touched, on exactly the endpoints where one tenant's inventory, credentials, and migration assessment live. Total elapsed time from legitimate signup to cross-tenant write: a few minutes of changing one number in a path.

What we observe in customer environments

We are honest about the limits of our visibility. CelvexGroup's continuous validation runs against assets and surfaces a customer scopes in, and our probes are deliberately read-only and benign, carrying an X-Celvex-Probe attribution header so the customer's SOC can always identify our traffic. We do not mutate other tenants' objects in production. What we do fingerprint, structurally and with two-identity comparison against scoped staging, is whether a token's scoping claim is actually compared to the resource a request names. Across web and API engagements over the past nine months, the rough breakdown on token-protected multi-tenant APIs:

The honest read: signature-valid-is-not-authorized is one of the highest-impact auth findings we ship against APIs, because the signature pipeline being correct produces a false all-clear. A clean token-validation review and a tidy key-rotation story give the wrong confidence, because the missing control sits one comparison below both, in the handler, where only the application knows which resource the request targeted.

What to do about it: the claim-binding contract

The fix is not a library upgrade; the library already validates the signature correctly. It reduces to one contract every token-protected, multi-tenant endpoint must satisfy, and the controls are cheap relative to the blast radius.

Claim-to-resource binding contract: controls that end the class

A valid signature proves the token is real. A claim-to-resource check proves the token is for this resource. Verifying the first and skipping the second hands every token holder everyone's data.

The audit, in concrete terms, starts by listing every handler that takes a resource id and checking each for a claim-binding comparison:

# Find handlers that take a resource id but never compare it to the token claim
# (the claim-confusion precondition). Grep the routes, then read each hit.
$ grep -rnE "source_id|tenant_id|org_id|/\{[a-z_]+_id\}" handlers/ \
    | grep -vE "claims\.|token\.|== *requested|assert_owner" \
    && echo "^ endpoints to review for missing claim-to-resource binding"

# Confirm the binding assertion is present on every write verb, not just reads
$ grep -rnE "func .*(Update|Create|Delete|Patch)" handlers/ -A 30 \
    | grep -B2 -A2 "source_id" | grep -v "claims"
# Any handler operating on a requested id with no claim comparison is a candidate.

Read each flagged handler. Confirm the token's scoping claim is compared to the resource the request names before the operation runs, on reads and writes alike. The exercise is finishable in a day for a single API, and it converts the highest-blast-radius bug in token-protected multi-tenant services into a closed door. For the API-side controls behind this contract, see our API security capability.

How Celvex catches this

Find. Prove. Fix. Verify.

Find

The scanner enumerates token-protected endpoints that name a resource id and fingerprints whether each compares the token's scoping claim to that id, using read-only structural checks and two-identity comparison against scoped staging, every request attributed with an X-Celvex-Probe header.

Prove

For a confirmed claim-binding gap we ship a signed Proof Capsule against a two-tenant fixture: a valid tenant-A token, the request naming a tenant-B resource, the response showing the cross-tenant write, and the missing claim comparison, Ed25519-signed and reproducible offline.

Fix

The Capsule's remediation block points at the claim-to-resource control scoped to the failing handler: the assertion to add comparing the claim to the requested id, or the server-side ownership load to wire in before the operation.

Verify

After the fix lands, the two-token probe shows a valid tenant-A token naming a tenant-B resource is rejected. The finding closes automatically and the dashboard records the verified-fix event for the audit trail.

Where we sit on the autonomy curve: at L1.5 today, our API-security track fingerprints the missing-claim-binding shape on token-protected multi-tenant APIs, runs the two-identity comparison with two genuinely valid tokens on scoped staging, and ships a reproducible Proof Capsule for each confirmed cross-tenant reference. At L2 within 90 days, the corpus extends the two-token probe across full CRUD per resource type, so a missing binding on any single verb surfaces even when the others are correctly scoped, and across agent and machine-to-machine token profiles. At L3 within twelve months, the scanner infers the scope claim and the resource-naming convention in unfamiliar token-protected APIs it fingerprints in customer environments and synthesizes the binding probe for each. We do not claim L3 today. We do claim our L1.5 catches the claim-confusion shape above and ships a signed Capsule for each.

Bottom line

The cross-tenant write that collapses an entire customer base does not need a forged token. It needs a real one and a missing comparison. CVE-2026-53471 is the 2026 reminder that a valid signature is authentication of the token, not authorization for the request, and that the check which binds a token's source_id claim to the resource a request names is the one most easily left out, especially on agent and machine-to-machine APIs that feel pre-trusted. The fix is a contract: bind every authorization decision to the subject or scope claim, check ownership server-side on the object you are about to touch, hold machine tokens to the same per-resource rigor as user tokens, and verify isolation with two valid tokens, not one. Until you compare the claim to the resource, a flawless signature pipeline is one changed resource id away from a tenant-wide breach.

Verifiable security. Find it. Prove it. Fix it. Verify the fix held. That is what we ship.

Sources

Probe your own token-protected APIs.

Free Exposure Check, no signup required. We test whether your endpoints bind each token's claim to the resource it names, and ship a signed Proof Capsule for the highest-confidence cross-tenant authorization gap.

Run a Free Scan →