Skip to content

+ Intertrace Threat ResearchTLP:CLEAR6 sources

Threat research10 min read

Approved is not executed: replay, substitution and check-to-use gaps in agent approvals

A person approves one action and a different one runs: edited arguments, re-resolved recipients, replayed permits, stale authority, duplicate retries. A step-by-step teardown of one approved payment that went wrong twice, and the bindings and tests that close the check-to-use window.

How attacks on AI agents work in practice, from public incidents and our own test corpora, and the controls that stop them.

September 30, 2026 · 10 min read

ApprovalsReplay AttacksTOCTOUExecution PermitsIdempotency
Contents
  1. 01The teardown: one approval, two losses
  2. 02Seven gaps, three families
  3. 03Signed, unexpired and unused is not the same as correct
  4. 04Fix the meaning: bind the resolved arguments, canonically
  5. 05Fix the authority: consume once, atomically, and re-check at dispatch
  6. 06Fix the effect: one approval, one payment at the provider
  7. 07Run the teardown against your own approvals
  8. 08What passing proves, and what it cannot

A finance lead approves an agent's request to pay a supplier $4,800. Three hours later a payment for $4,800 leaves the company, to a different bank account. Then a second one follows. Every log line says the payment was approved, and in a narrow sense it was. Nobody checked whether the thing that executed was the thing the person saw.

MITRE's CWE-367 describes the pattern: a product checks a resource's state before using it, "but the resource's state can change between the check and the use in a way that invalidates the results of the check" [1]. Agent approvals are a generous version of it. The check is a human decision that takes minutes or hours, the use happens in another process, and in between sit queues, workers, model re-planning and external systems that change on their own. If an approval is just an ID that says yes, nothing about the action survives the trip. Our position: an approval has to carry what was approved, for which agent, at which service, once and within a time limit, and dispatch has to check all of it.

The teardown: one approval, two losses

Brightwater Supply is a fictional distributor. Its accounts payable agent matches invoices to purchase orders and proposes payments, which a finance lead approves in a chat notification showing the supplier name, invoice number and amount. Approved payments queue for an afternoon batch. The payment tool takes a supplier ID and an amount, and resolves the bank account from the supplier record when it runs.

  1. 10:05. The lead approves $4,800 to a long-standing supplier. What the lead saw was a summary; the destination account was never on screen and never part of the approval.
  2. 11:40. The agent processes an email, apparently from that supplier, announcing new bank details, and updates the supplier record. That is within its permissions.
  3. 14:00. The batch dispatches the approved payment. The tool resolves the supplier ID to the new account, and the money goes there. The approval ID was valid, unexpired and unused, which is all dispatch checked.
  4. Then the bank API times out. The worker retries with a fresh request, and a second $4,800 follows.
The check-to-use window in an agent approvalFig. 1
Proposeagent drafts callReviewperson sees a viewApprovethe checkQueuecall waitsDispatchthe useProvidereffect happensCHECK-TO-USE WINDOWView ≠ argumentsapprover saw a summaryArguments changeor an alias re-resolvesPermit replayedtwice, or at another serviceLost responseretry makes a second effectthe person approved at the green step · the effect happens at the black step · anything that changes in between is unapproved

The approval is the check and the dispatch is the use. Four failure points sit around that window: what the approver saw differs from the arguments, the arguments or their targets change while the call waits, the permit is replayed or presented elsewhere, and a lost response triggers a second effect.

Three of the figure's four failure points appear in that timeline; only replay is absent. The approval was an ID, and the properties it needed, the resolved destination, a single use, a binding to the request that would actually go out, did not travel with it.

Seven gaps, three families

Brightwater hit three of the seven gaps below. The table lists all of them with the precondition that opens each and the binding that closes it.

Check-to-use gaps in agent approvals, what enables them and what closes them
GapPreconditionWhat happensBinding that closes it
Argument substitutionDispatch checks only that an approval ID existsThe agent, or text steering it, changes the amount, recipient or payload after approvalHash of the canonical arguments in the approval, recomputed and compared at dispatch
Render mismatchThe approver sees a summary or truncated viewApproval is given for what was displayed, not what will runDisplay derived from the same canonical arguments that are hashed, with full values for material fields
Reference driftArguments name an alias, list, vendor ID or latest versionThe reference resolves to a different target at dispatch than at reviewResolve references before review and bind the resolved values, or re-ask if they change
ReplayThe permit can be presented more than onceOne approval authorizes several executionsUnique permit ID consumed once, atomically, at the enforcement point
Cross-audience reuseServices share a signing key or parserA permit issued for one service is accepted by anotherAudience bound in the permit and checked by the receiver
Stale authorityThe call waits while the agent is disabled or policy changesAn approval outlives the authority it assumedShort expiry plus an online check of agent and grant state at dispatch
Retry duplicationThe provider's response is lost and the worker retries with a new requestOne approved payment or message becomes twoIdempotency key derived from the permit, with reconciliation before any retry

The first three are about meaning: approval and execution describe different actions. The next three are about authority: the right action runs under a permission that should no longer apply. The last is about effects: the right action under the right authority happens twice. A control for one family does nothing for the others, which is how approvals that pass design review still fail in production.

Signed, unexpired and unused is not the same as correct

Proof-of-possession tokens are the obvious reach, and they solve a neighboring problem. DPoP, RFC 9449, makes a client prove it holds a private key on each request; the proof carries a unique jti servers can use for replay detection within a limited acceptance window, plus optional server-supplied nonces [3]. The RFC states the limit: the proof "only contains claims for the HTTP URI and method, but not the message body," which leaves it potentially open to replays where an attacker can modify message contents [3].

JWT supplies the building blocks for a permit: aud names who may accept it and must be checked by the receiver, exp sets when it stops being acceptable, and jti is a unique identifier that "can be used to prevent the JWT from being replayed" [4]. None of those claims describes the business action. A signed, unexpired, audience-correct, never-seen-before permit would have authorized Brightwater's payment to the wrong account, because the account was not inside it.

Fix the meaning: bind the resolved arguments, canonically

The binding that closes substitution is a hash of the arguments the person approved, carried in the approval and recomputed from the request at dispatch. Hashing raw JSON is fragile, because one object can serialize many ways. RFC 8785, the JSON Canonicalization Scheme, defines one representation for hashing and signing: properties sorted recursively by UTF-16 code units, array order preserved, numbers serialized by the ECMAScript rules, no whitespace, UTF-8 output [5].

Canonical JSON is necessary and not sufficient. JCS does no Unicode normalization, so visually identical strings can hash differently, and it gives no special treatment to dates or large numbers, which it recommends encoding as strings [5]. Nor can it know that "finance-team" is a distribution list whose membership changed overnight, or that a supplier ID resolves to a bank account at run time. Resolve aliases, lists, references and defaults into concrete values before the approval is shown, hash those, and show the approver the same values that were hashed. At Brightwater, the destination account in the hash would have failed comparison at 14:00 and asked again.

One more fix sits outside the permit. Changing a supplier's bank details changes where future money goes, so it should itself be a consequential action that asks a person. An agent that can edit supplier records can otherwise poison the next approval.

What a permit must bind, and what each binding stopsFig. 2
PERMIT · ONE APPROVED ACTIONWHAT THE BINDING STOPSagent + tenantuse by another agent or tenantaudacceptance at a different serviceactionsame approval, different operationargs_sha256 (JCS)arguments edited after approvalresolved targetsan alias or list changing meaningexpan approval used hours laterjti · consumed oncereplay and concurrent double usepolicy_versiona decision made under old rulessigned by the issuer · checked at dispatcha signature proves the permit was not edited · only the bound fields make it mean one action, once

A reference permit for one approved action. A signature proves the permit was not edited; each bound field is what makes it mean one action, by one agent, at one service, once, within a time limit and under a known policy version.

Fix the authority: consume once, atomically, and re-check at dispatch

OWASP's AI Agent Security Cheat Sheet puts it in one line: "Use short-lived authorization artifacts and replay protection for irreversible operations" [2]. Replay protection is a state change, not a lookup. If two workers both read "unused" and then both mark it used, both dispatch. Consumption needs a single atomic compare-and-set at the enforcement point, scoped to issuer and audience, and a defined behavior when the shared state is unavailable: for consequential actions, hold the action rather than accept an unverifiable permit. Pair it with a short expiry and an online check of the agent and its grants at dispatch, so an approval cannot outlive the authority it assumed.

Fix the effect: one approval, one payment at the provider

Atomic consumption guarantees one dispatch through your gate, not one effect at the provider. A worker can consume the permit and crash before sending, or send and lose the response, which is what happened at 14:00. Stripe's documentation is a useful model of the semantics needed. It saves the status code and body of the first request made with an idempotency key and returns that saved response on retries, including 500 errors. It compares incoming parameters with the original request and errors if they differ. Keys can be pruned once they are at least 24 hours old, after which a reused key starts a new request [6]. The parameter comparison is exactly the property an approval needs; the pruning window is a reminder that a retry long after the fact may not be protected.

Derive the idempotency key from the permit, so every retry of the approved action carries the same key and a genuinely new action needs a new approval. When the outcome is ambiguous, ask the provider about the original request before anything else. Never let the agent conclude from conversational context that a payment probably failed and issue it again.

Log both ends of the window

For every approved action, record the hash shown to the approver, the hash computed at dispatch, the permit ID and its consumption time, the policy version at approval and at dispatch, the provider request ID and idempotency key, and the reconciliation result. Alert on any dispatch whose hash differs from its approval, any permit presented after consumption, any audience mismatch, and any provider showing two effects for one permit. Keep sensitive argument values in restricted storage and put only the hash and masked fields in ordinary telemetry.

Run the teardown against your own approvals

Build a fixture provider that records every accepted effect and can delay, drop or duplicate responses. Approve one synthetic action, run each case, and compare your decision log with the fixture's count of effects. This matrix is proposed, not measured.

  • Change each material argument independently after approval (amount, currency, recipient, destination account, payload, connection). Each is refused or re-asked.
  • Change what an alias or list resolves to between approval and dispatch. The call asks again.
  • Render the approval with a long or Unicode-confusable value. The approver sees the full value that was hashed.
  • Present the same permit concurrently from several workers and instances. Exactly one dispatch.
  • Present a permit issued for one service to another that shares its signing key. The audience check refuses it.
  • Disable the agent, and separately change its policy, while an approved call is queued. Dispatch follows your documented rule.
  • Make the consumption store unavailable. Consequential actions hold rather than proceed.
  • Drop the provider's response after a successful effect and let the worker retry. The fixture records one effect.
  • Run the benign twins: a legitimate retry with the same key succeeds, and a legitimate second payment with a new approval is not blocked as a replay.

What passing proves, and what it cannot

Passing establishes a property under the conditions you tested, not under arbitrary provider behavior. Some providers offer no idempotency mechanism and no way to look up a prior request; for those, the honest design is a manual reconciliation path and an approval screen that says duplicates are possible. Binding arguments also cannot make an approval wise. A person can still approve the wrong thing. The goal is narrower and checkable: what runs is what was approved, once, by the agent that asked, under authority that still holds.

Sources

Reporting, research and specifications this piece draws on. Links go to third-party sources; analysis and recommendations are Intertrace’s own.

  1. 01CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition · MITRE
  2. 02AI Agent Security Cheat Sheet · OWASP Cheat Sheet Series
  3. 03RFC 9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP) · IETF
  4. 04RFC 7519: JSON Web Token (JWT) · IETF
  5. 05RFC 8785: JSON Canonicalization Scheme (JCS) · IETF
  6. 06Idempotent requests · Stripe API Reference

About this desk

Intertrace Threat Research

Threat research

How attacks on AI agents work in practice, from public incidents and our own test corpora, and the controls that stop them.

All 18 pieces by Intertrace Threat Research

Dated September 30, 2026 · added to intertrace.ai October 17, 2026

+ Runtime enforcement

See these controls on your own agent traffic.

Intertrace sits inline between your agents and the models, tools and MCP servers they call, and decides every call before it runs.

Keep reading

All insights →