CoreBreak: Tool Execution Without a Model Turn in Three Runtimes
Researchers showed that several production agent runtimes would run a tool without a model ever authorizing it. No jailbreak, no poisoned web page, no clever prompt: the model never got a turn at all. How you got there differed by vendor, and that part matters more than the headlines suggested. AWS accepted a tool call the caller had written by hand. Google trusted an approval that an attacker had tampered with. Vercel trusted a program inside its own sandbox on the strength of what its command line looked like. Different inputs, one missing check, and the same result: every guardrail you placed around the model was bypassed, because the model was never in the path to be guarded. Scope matters here and the headlines mostly dropped it, and the three do not share a precondition. AWS needs a caller who has already authenticated to the harness. Google needs an attacker who can get events into an agent’s session history, and whether reaching that takes authentication depends on how you built the thing: Google itself scores the flaw as requiring no privileges. Vercel needs three things together: Linux, an active harness session exposing at least one host-provided tool, and untrusted code already running inside the sandbox. What changed for you depends entirely on how you deployed. Vercel went first and published patched packages on July 10, Google shipped ADK for Python 2.5.0 on July 16, and AWS had a server-side fix on its managed API by July 31 with no customer action. If you built on the open-source Strands Python SDK, or on AgentCore Runtime instead of the managed harness, nothing was fixed on your behalf and the validation is yours to write.
Four CVEs, three vendors, one missing check. Amazon Bedrock AgentCore carries CVE-2026-18830, CVSS v4 8.6 (AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N), CWE-1287: when the final message of an InvokeHarness request carried a toolUse content block, the agent event loop dispatched the named tool without model mediation. Google ADK for Python is CVE-2026-18236, CVSS v4 9.3, CWE-863, where the confirmation processor never verified that the target tool belonged to the executing agent, that the tool required confirmation in the first place, or that its name and arguments matched the call recorded in the session; a second path in resumable mode accepted function_call parts inside user-authored events. Vercel’s @ai-sdk/harness-codex (CVE-2026-64650) and @ai-sdk/harness-opencode (CVE-2026-64651) both scored 6.3 for a relay that read /proc and authorized any Linux process whose command line contained the path of an approved helper script, letting sandbox-resident code reach host-exposed tools without a model-authorized event. Both were fixed on July 10, in 1.0.29 and 1.0.28 respectively, and published within the same minute. Track them as one release train rather than two independent patch levels. Different languages, different codebases, one authorization failure: dispatch could proceed with no trustworthy binding to a model-authorized event. AWS and Google accepted attacker-influenced structured state; Vercel inferred authority from a sandbox process’s command line, which is the same failure wearing different clothes.
TL;DR
- Four CVEs across AWS Bedrock AgentCore, Google ADK for Python, and two Vercel
@ai-sdk/harness-*packages converge on one authorization failure: tool execution could proceed with no trustworthy binding to a model-authorized event, so every guardrail attached to the inference call was routed around. The preconditions differ, and collapsing them is the mistake the coverage made. AWS needs an authenticated caller, Google needs the ability to inject events into session history and is scored PR:N, Vercel needs Linux, a live session exposing host-provided tools, and untrusted code already running in the sandbox.- Google’s CVE-2026-18236 (CVSS 9.3) is the one for ISSOs: ADK’s confirmation processor never checked that the approved tool belonged to the agent, required confirmation, or matched the recorded arguments. Before 2.5.0, an approval record could not by itself prove that the tool and arguments which ran were the ones presented for approval. File the resumable-mode
function_callpath as a separate POA&M finding from the forgery path.- Build a detection that flags any tool-execution span with no qualifying model-invocation event earlier in the same turn. Preserve timestamps or parent/child relationships; do not collapse the trace to a flat set of span names. Validate it against a known-good trace first: span names and
gen_aiattributes vary by framework and instrumentation version, and ahas_toolclause that stops matching returns zero rows forever, indistinguishable from a clean environment.- If you built on the open-source Strands Python SDK or on AgentCore Runtime rather than the managed harness, nothing was fixed for you. Strands documents message history as trusted input, the event loop still skips model invocation when the last message carries a ToolUse block, and AWS says of non-harness Runtime deployments that Runtime “provides no server-side protection” against that input. Read that narrowly: Runtime still supplies IAM/JWT authentication and microVM isolation, and what it does not do is validate your payload semantics. Grep for
payload.get("prompt", "Hello"), enforce the string type, and reject caller-suppliedtoolUse/function_call/tool_callsat the boundary and in replayed session events.- AWS’s server-side fix shipped with no version, patch level, or diff to record, so generate your own CA-7 evidence: send a benign
InvokeHarnessrequest whose final message names a nonexistent tool in atoolUseblock, keep the validation response, and confirm in the corresponding trace that no tool-dispatch span occurred. Run it quarterly per harness ARN. CloudTrail data events omit the message body, so CloudTrail alone cannot carry this evidence and your AU-3 implementation has to name what does.
The step where authority was supposed to be established
Normal flow is four steps. The SDK sends system prompt, history, user message, and tool definitions to the model. The model returns a structured instruction naming a tool and its arguments. The SDK executes it. The result goes back for the next turn.
Steps two and three are where authority lives, and none of the vulnerable runtimes checked that the thing they were about to execute came out of step two. Hedi Ingber and Aviyam Ivgi, who presented this as CoreBreak at Black Hat USA 2026 and whose findings The Hacker News wrote up on August 6, draw a deliberate line between this class and prompt injection. Worth stating plainly before anyone escalates on it: all four came through coordinated disclosure rather than incident response, AWS credits the pair through that process, and Google’s CVSS vector carries an exploit-maturity value of Proof-of-Concept. Nothing in the public record puts any of this in the wild. That is a reason to schedule the work properly rather than a reason to skip it.
The researchers’ line between this class and prompt injection is the right one. Prompt injection is a persuasion problem in the model’s context window; you defend it with input filtering, output filtering, and instruction hierarchy, all of which sit at the model layer. Here the model layer is inert. Your Bedrock Guardrails config, your system-prompt hardening, your “the agent will refuse destructive operations” evaluation suite — all of it is measuring a component that the request routed around.
That reframing has a consequence for how you spend the next quarter. If your AI security program is a stack of controls attached to the inference call, it has a hole underneath it the exact size of your tool registry.
The forgeable approval
Google’s is the one that should make ISSOs uncomfortable, and 9.3 is not an inflated score.
ADK’s confirmation flow is the mechanism a lot of shops are using to satisfy “a human approves high-impact actions.” When a tool requires confirmation, ADK emits an adk_request_confirmation function call with a function_call_id, a hint, and a payload; the approver POSTs a FunctionResponse back with confirmed: true, a matching id, and, in resumable mode, the invocation_id of the paused run. That exchange was the control. The gap was not authentication of the HTTP caller. It was the authorization and integrity of the continuation event itself. The processor took the response at face value without confirming that the tool named in it was registered to the executing agent, that it was a tool gated by confirmation at all, or that its arguments matched what the model had actually asked to run.
So an attacker positioned to inject events into session history could approve a call that was never made, or approve a call that was made and swap the arguments underneath it. Be careful how you write that up. It does not mean every approval record your organization holds is forged. It means that before 2.5.0 the record could not, on its own, establish the binding: ADK never enforced that the tool and arguments which executed were the ones presented for approval. AU-10 non-repudiation resting on a token the framework declined to verify is a thinner artifact than the control narrative around it suggests.
Upgrade to ADK 2.5.0 or later. If you’re on resumable mode, treat the confirmation-forgery path and the user-authored function_call path as two separate findings in your POA&M — they were fixed in the same release but they are not the same bug, and an assessor who reads only the CVE will see one.
What your audit record can and cannot tell you
Here’s the question the coverage skipped. After you patch, how do you prove a given tool execution was authorized by a model turn?
For the managed AgentCore harness, AWS documents that every invocation emits traces, logs, and metrics automatically, with model calls, tool invocations, memory operations, and shell commands each appearing with timing and payloads. Good. Two caveats before you build on that. First, you have to enable Transaction Search in CloudWatch once per account or you see no traces at all, and that step gets skipped constantly because nothing errors when it’s missing: the console just looks empty and everyone assumes the agent is quiet. Capture spans as structured logs when you turn it on. The trace-summary indexing percentage is a separate control and a common source of confusion, because it governs how many ingested spans X-Ray indexes as searchable trace summaries, not whether captured spans are retained as structured logs at all. While you’re in there, confirm where yours are retained. Depending on Region and configuration, AgentCore writes spans either to a stream in the agent’s own log group, /aws/bedrock-agentcore/runtimes/<agent_id>-<endpoint_name>, or to the shared aws/spans group, and pointing your ingestion at the wrong one produces exactly the same silent nothing as never enabling the feature. What can genuinely cost you a span sits upstream of that: head sampling, incomplete instrumentation, exporter loss, a broken subscription filter, or ingestion loss on the way into Splunk. An absence-of-span detection is only as sound as your evidence that those paths retain model and tool spans at the rate it assumes.
Second, the telemetry concepts page says default span output covers memory resources only and that you need to instrument with ADOT, the AWS Distro for OpenTelemetry, for everything else. That is less a contradiction of the harness page than a different scope: one describes the managed harness, the other describes agent code you instrument yourself. Which of them governs your deployment is exactly the ambiguity worth settling first. Go pull one real trace out of your own account with agentcore traces get <trace-id> --harness <name> and see what’s actually in it before you write a detection against either description.
CloudTrail is worse than you’d hope. Harness data-plane calls land as InvokeAgentRuntime and InvokeAgentRuntimeCommand under resources.type = AWS::BedrockAgentCore::Runtime, with no harness-specific event name at all. Any correlation search you keyed on the string “Harness” returns nothing. What CloudTrail reliably gives you is that the invocation happened and who made it. What you should not assume is that it carries the application-level message content this test needs. AWS documents payload omission explicitly for InvokeAgentRuntimeCommand, and I could not find equivalent current documentation extending that to every InvokeAgentRuntime record, so check the event schema in your own account before repeating it as a universal. If the content is absent there, the trace or an application-level audit record has to supply it. Either way, treat payload omission as a limit on what CloudTrail can serve as evidence, not as a finished AU-3 finding. AU-3 governs the content of your audit records as a whole, so the honest sequence is to establish whether harness traces or application-level audit records supply what CloudTrail omits, and only then write up a deficiency. If nothing supplies it, you have one.
The detection you actually want is each tool-execution span for which no qualifying model-invocation event occurred earlier in the same turn. The ordering is the whole point, and it is the part that is easiest to lose. Collapse a trace into the set of span names it contains and you have discarded the sequence, at which point an unauthorized dispatch followed later by a perfectly ordinary model call is indistinguishable from a clean trace. Normal agent loops do make further model calls after a tool result, so that is not a hypothetical distinction. Rough shape, against CloudWatch logs shipped into Splunk, keeping the timestamps. Treat it as an illustrative first pass and not a rule to ship: validate it against a known-good trace containing a real tool execution from your own instrumentation, and read the limitation directly below it before you trust the output. The index and sourcetype are placeholders, so point them at your own telemetry routing.
index=aws_agentcore sourcetype="aws:cloudwatchlogs"
| eval is_model=if(match(span_name, "(?i)chat|invoke_model|gen_ai"), 1, 0)
| eval is_tool=if(match(span_name, "(?i)tool"), 1, 0)
| eval model_time=if(is_model=1, _time, null()), tool_time=if(is_tool=1, _time, null())
| stats min(model_time) as first_model, min(tool_time) as first_tool by trace_id
| where isnotnull(first_tool) AND (isnull(first_model) OR first_tool < first_model)
Be blunt about what that does and does not catch, because this is the block people will actually paste. It is a trace-level canary for one specific shape: a trace whose first tool span precedes every model span, or which contains no model span at all. It is not the per-dispatch rule stated above, and the gap is not academic. Because it reduces each trace to first_model and first_tool, a single legitimate model span early in a trace satisfies the model side of the test permanently, so an unauthorized second or later tool execution in that same trace will not be detected. AWS defines a trace as a request-level record containing ordered spans, so a long agent loop is exactly where that blind spot lives. Treat this as a narrow first-pass detector and nothing more. The stronger rule correlates every tool span to its qualifying parent, its immediately preceding authorization event, or an explicit authorization identifier where the runtime exposes one.
Do not paste it and expect it to work. Span names and attributes vary by framework, by instrumentation library, and by OpenTelemetry generative-AI semantic-convention version, so inspect a real trace from the exact deployment before you fix anything to particular gen_ai keys. If your own instrumentation names the model span something is_model never matches, custom_llm_call say, every trace you produce becomes an alert on day one. The mirror-image failure is quieter and considerably worse: if is_tool never matches your tool spans, the search returns zero rows every day and is indistinguishable from a clean environment. Validate the rule against a trace you know contains a tool execution before you let an empty result stand as evidence of anything.
Volume and tuning. Search cost here scales with spans per session, retention, index design, and your own licensing and compute model, and not one of those generalizes from somebody else’s deployment. Measure span volume in the environment you are actually running before you schedule anything against it. At high volume, summary-index or pre-aggregate the fields the correlation needs rather than regrouping the entire raw trace corpus on every run.
The false positives are the interesting part, and there are three real sources. The first is the documented inline-function flow, and AWS’s own documentation spells it out precisely: the harness accepts a toolResult block in the final message, the model then resumes reasoning over that result, and inline function tools work by having the assistant’s toolUse message followed by the toolResult in the same request, so the toolUse block is not the final one. Two things follow from that. A rule alerting on tool-shaped content anywhere in the request body fires on every legitimate inline-function call, which is your entire first week of tuning. And the discriminator is not the block type alone but its role and position: a caller-authored toolUse in the final message is the abuse shape, while an assistant toolUse followed by a user toolResult is the documented one. Match the sequence, not the substring.
Cross-trace continuations are the second, and here you should be more careful than the first version of this post was. It is easy to assert that resumed sessions put the authorizing model call in an earlier trace, and I have not found AWS documentation establishing that as generic harness behavior. The documented inline-function flow actually runs the other way: the client executes the function between invocations, and the next request carries the assistant toolUse plus the user toolResult for the model to resume from. So treat cross-trace splitting as something to confirm per tool type rather than infer from session reuse, and where it genuinely happens the orphan-span logic will flag it by design. And if the pipeline samples or drops spans anywhere ahead of the dataset your search reads, a missing model span looks exactly like the attack, at which point the detection is measuring your telemetry rather than your agents. Settle end-to-end span completeness before you tune the threshold, or you will spend a month chasing your own instrumentation.
Strands, and the branch that is still there
The managed fix does not extend to the open-source SDK underneath it. As of early August the Strands Python event loop still carries a branch commented Skip model invocation if the latest message contains ToolUse, in strands-py/src/strands/event_loop/event_loop.py, which sets stop_reason = "tool_use" and proceeds straight to execution when the last message already contains a tool-use request. TechTimes covered the gap: no separate CVE, no affected-version range, no patched release to upgrade to.
What there is, and it is a better citation than the press coverage of it, is Strands’ own Trusted Message History guidance, which documents the behavior as a security property instead of leaving you to infer it from source. It says plainly that an agent treats its message history as trusted input, and that in the Python SDK a tool-call block as the most recent message “causes the agent to run that tool directly on its next invocation, with no model call in between,” with the block’s author choosing the tool and its arguments outright. If you are arguing this internally, cite that page rather than the CVE coverage. It is the vendor describing its own trust boundary.
Be fair about what that branch is, though. It’s a resume mechanism, and it’s legitimate when agent.messages only ever contains messages the framework itself produced. It becomes a vulnerability at the moment caller-supplied content reaches that list unfiltered, which is a property of your entrypoint rather than of the SDK. That distinction matters for how you write it up, because “unpatched RCE in Strands” will get you argued with, and “our entrypoint passes untrusted structured input into an event loop that treats tool-use blocks as authoritative” will not.
That same page carries a second exposure the CVE coverage skipped, and this one is not Python-specific: forged tool result content. A toolResult block you did not produce, sitting in history that reaches the model, can misrepresent what a tool returned and steer the model’s next step. Nothing dispatches directly off it, so it doesn’t resemble the headline bug, but it is the same trust boundary and Strands calls it a concern in both SDKs. If you are auditing message history, audit it for both block types rather than just the one with a CVE attached.
The remediation sits on your side of AWS’s shared-responsibility boundary, and AWS is unusually direct about where that line falls. Its own harness security page states that the boundary is “IAM or JWT authentication combined with microVM isolation,” and that “any principal that passes that gate reaches the tools and capabilities configured on the harness, which makes caller authorization and input validation a customer responsibility.” For non-harness Runtime deployments it goes further and says Runtime “provides no server-side protection” against caller-supplied content blocks. Take that in its context rather than as a claim about Runtime security generally: AWS owns the infrastructure, the microVM isolation, the kernel and runtime patching, and the structural validation of what InvokeHarness accepts. It does not own the semantics of your payload. Your entrypoint does, so enforce the type rather than defaulting it:
@app.entrypoint
def invoke(payload, context):
user_message = payload.get("prompt", "")
if not isinstance(user_message, str) or not user_message.strip():
return {"error": "Invalid input: 'prompt' must be a non-empty string"}
return {"response": agent(user_message).message}
payload.get("prompt", "Hello") is the pattern to grep your repos for, and be clear about what separates it from the version above, because it is not the default string. A default only fires when the key is absent. When the caller does supply prompt, .get() hands back whatever they sent, a list of content blocks included, and with no isinstance check on the next line nothing stops it reaching the agent. The default is what makes the line look considered; the missing type check is the bug. If you accept structured message arrays for multi-turn, strip toolUse out of anything user-supplied at the boundary, or fail the request outright, which is the easier of the two to put in front of an assessor:
def reject_caller_tool_use(messages):
for m in messages:
for block in m.get("content", []):
if "toolUse" in block:
raise ValueError("caller-supplied toolUse block rejected")
return messages
The key name is protocol-specific, so port the check rather than copying it. On ADK- or OpenAI-shaped payloads the same block arrives as function_call or tool_calls, and on the ADK resumable path the untrusted content can already be sitting in session history rather than in the request you’re currently inspecting, so filter the events you replay and not only the ones you receive.
If you strip instead of rejecting, there’s a trap in it that Strands documents and most implementations miss. The dispatch check inspects only the last message, so you have to keep stripping until the final message carries no toolUse block at all: drop a single message that contained nothing but a toolUse and you can expose another one sitting directly beneath it. A one-pass filter that removes the offending message and stops is not equivalent to that loop. Which is the practical argument for rejecting outright wherever you can afford to, beyond it being the easier thing to put in front of an assessor. There is no iteration to get wrong.
And while you’re in there, the same page documents that additionalParams, apiBase, modelId, and per-invocation skills overrides are all passed through unvalidated, which means a caller who can reach your harness can also redirect inference or point the agent at their own S3 skill bundle. Same trust-boundary failure, different field.
Verifying a fix that shipped with no artifact
The AWS bulletin says the mitigation applies automatically and no customer action is required. For an ISSO assembling evidence, that’s a URL and a vendor assertion. There’s no version to record, no patch level to screenshot, no diff to review. The bulletin does supply one piece of scoping worth putting in the risk statement: impact was limited to the tools configured on a given harness, and a harness with no configured tools could not execute any tool at all. Your blast radius was your tool registry, which is the same argument the CM-7 row below makes.
You can generate the artifact yourself, and you should. Send a benign InvokeHarness request whose final message contains a toolUse block naming a tool that doesn’t exist on the harness. The documented behavior is that the harness refuses the block without ever evaluating which tool it names, which is what makes a nonexistent tool name a safe probe. Keep the server-side validation response, then confirm in the corresponding trace that no tool-dispatch span occurred. The response on its own tells you the request failed. The response paired with the trace tells you it failed before anything ran, which is the claim you are actually making. Run it quarterly against each harness ARN, put both artifacts into your continuous monitoring evidence, and you have something for CA-7 that doesn’t reduce to “AWS said so.”
Author-derived mapping, not official control language. Use it as a starting point for POA&M wording and evidence collection rather than as an authoritative crosswalk.
| NIST SP 800-53 control | What this touches |
|---|---|
| AC-3, AC-6 | Tool dispatch enforced at the executor, tied to the model turn that requested it |
| AU-3, AU-12 | Trace records that distinguish model-originated from caller-supplied tool calls; confirm what CloudTrail actually carries in your account before relying on it for that content |
| AU-10 | Before 2.5.0, ADK did not enforce the binding between a confirmation and the original registered tool call and its arguments; under manipulated session history the approval record alone could not prove the action executed was the action approved |
| SI-10 | Entrypoint validation of caller-supplied content blocks and model config fields |
| CM-7 | Tool registry as an attack-surface inventory; the allowedTools set is the blast radius |
| SI-2, SR-3 | Track and remediate the vulnerable dependency versions (Vercel packages pinned below 1.0.29 / 1.0.28) and manage the supplier relationship around Strands, which has a known pattern and no fix. SR-4 belongs here only where you are specifically documenting component provenance |
Where the guardrails should have been
The uncomfortable read is that a whole industry of AI security tooling attached itself to the inference call because that’s where the interesting demos are. Content filters, injection classifiers, instruction-hierarchy enforcement — real controls, all of them, all sitting one layer above the thing that actually executes.
Put the authorization at the executor. A tool invocation should be refusable unless it carries a reference to the specific model response that produced it, and the executor should verify that reference rather than infer it from message shape. AWS’s Gateway policy engine gets partway there with Cedar rules on who can call which tool with which arguments, and that’s worth configuring if your tools are served through it, though the policy still doesn’t know whether a model asked.
It would be a tidier ending to say nobody ships that binding. It stopped being true in July, and the exception is the most useful thing in this story. Vercel’s patched relays are precisely what this section is describing: both advisories state that the process-path authorization fallback is removed entirely, and that relay requests are now accepted only after an exact, short-lived, one-time authorization matching the tool name and input, derived from a bridge-observed model event. Short-lived, one-time, and bound to a specific observed model event is the entire design, stated in four clauses by a vendor that shipped it. AWS took the narrower route on the managed harness, rejecting caller-authored final-message toolUse blocks before the event loop, which closes this path without establishing provenance in general. So the binding is implementable, and inside those patched relays it is now the default behavior. What you still don’t get is a common execution-provenance primitive across runtimes: each framework defines and enforces that boundary its own way, and several still leave the material trust decisions to the application. That is a narrower complaint than nobody having worked out how, and a considerably more actionable one.
Until you have verified execution-time provenance and per-tool authorization for a particular runtime, work from the conservative model: treat an authenticated caller, or compromised sandbox code where that applies, as potentially able to reach any tool inside that runtime’s authorization boundary. Size the tool registry and the execution role accordingly.
Sources
- AWS, Google, and Vercel Agent Flaws Let Attackers Trigger Tools Without Running the Model (The Hacker News)
- Issue with Amazon Bedrock AgentCore harness — CVE-2026-18830 (AWS Security Bulletins)
- Security and access controls — Amazon Bedrock AgentCore (AWS Documentation)
- Security best practices for AgentCore Runtime (AWS Documentation)
- Observability and cost controls — Amazon Bedrock AgentCore (AWS Documentation)
- Understand observability for agentic resources in AgentCore (AWS Documentation)
- Transaction Search — Amazon CloudWatch (AWS Documentation)
- Get action confirmation for ADK Tools (Agent Development Kit)
- ADK for Python — confirmation-processor fix commit c03f333 (GitHub)
- Trusted Message History (Strands Agents documentation)
- event_loop.py — strands-agents/harness-sdk (GitHub)
- AI SDK Codex Harness Tool Relay Authorization Bypass — CVE-2026-64650 (GHSA-qw9h-448j-6rph)
- AI SDK OpenCode Harness Tool Relay Authorization Bypass — CVE-2026-64651 (GHSA-g48p-5rr5-8rgq)
- CVE-2026-18830 (THREATINT)
- CVE-2026-18236 (THREATINT)
- AWS Fixed Its Managed Agent Service but Left Strands Python SDK Unpatched (TechTimes)
This post was engineered and validated through a multi-agent AI workflow — drafted, adversarially reviewed by several independent models, checked against primary sources, and given a human review before publishing. See an inaccuracy, or found this useful? Leave a comment below — corrections and feedback are read and shape what comes next.