Summary
Microsoft.Agents.AI.Workflows.Declarative implements a protected-value
control for declarative agent workflows: values assigned from the
Environment scope carry SensitivityLevel.Sensitive, and every executor
that evaluates an expression destined for an external call checks the
evaluated result’s Sensitivity before transmitting. By late September 2026
that check covered seven executors, and the HTTP request executor had just
been added as an eighth.
The MCP tool executor — the action that invokes tools on a remote MCP
server — was not in the set. InvokeMcpToolExecutor evaluates its server
URL, server label, tool name, arguments and transport headers through the
same engine that computes the sensitivity tag, and uses only the value. A
workflow whose MCP argument or transport header referenced an Env-scoped
value therefore forwarded that value — no exception, no redaction — to the
MCP server the workflow itself named: the exact transmission the neighboring
checks exist to prevent, headed off-host.
The gap was reported to Microsoft on 2026-09-28 and fixed upstream the next
day as PR #8832 (“Reject protected values in declarative MCP invocations”),
which routes all five read sites through a checked accessor. The fix shipped
in Microsoft.Agents.AI.Workflows.Declarative 1.24.0; the advisory is
GHSA-9f26-7mqc-9vc4.
Background
Declarative workflows let an application describe an agent orchestration in
YAML and run it through Microsoft.Agents.AI.Workflows.Declarative. Workflow
expressions evaluate in a scoped engine: values from the workflow’s own state
are ordinary, while values provided by the host under the Environment scope
— connection strings, keys, host configuration — are tagged
SensitivityLevel.Sensitive.
The guard built on those tags is deliberate: an executor that would hand an evaluated value to an external system throws rather than transmit when the value is sensitive. The guard was extended round by round during September 2026, and in each round the fix added checks to the parameters that the particular investigation had identified — the HTTP request executor received its gate on 2026-09-28. The mechanism is a per-site hand enumeration, and that is where coverage gaps live — the MCP tool executor was the outbound path still missing its check.
The gap
Every expression input of InvokeMcpToolExecutor goes through the same
Evaluator.GetValue call that computes Sensitivity; the executor kept the
value and discarded the tag. grep -n "Sensitiv" InvokeMcpToolExecutor.cs
returned zero hits — no check anywhere in the file:
// GetArguments(): the evaluated value is stored…
result[argument.Key] = this.Evaluator.GetValue(argument.Value).Value.ToObject();
// GetHeaders(): …and the evaluated header string is returned…
string value = this.Evaluator.GetValue(header.Value).Value;
// …with .Sensitivity never inspected on either path.
The evaluated values go straight to mcpToolHandler.InvokeToolAsync(...),
and the shipped DefaultMcpToolHandler puts them on the network: the
evaluated headers become the MCP transport’s AdditionalHeaders and the
evaluated serverUrl is called over Streamable HTTP. The destination — the
MCP server — is named by the workflow definition itself.
Compare the HTTP request executor, which received its gate on 2026-09-28:
GetRequestValue() throws Cannot use a protected value in HTTP request
{location}. for every request field it evaluates. The MCP tool invocation —
also an outbound path, and one to a workflow-selected destination — had no
such check at all.
Verifying it
The differential was straightforward because the control next door is
exact: a sensitive Env value placed in a guarded parameter throws before
the handler is invoked; the same value in an MCP argument or transport
header was forwarded. With the MCP handler boundary mocked, a small test
file added to the repository’s own test project (product source unmodified)
showed the mocked handler receiving the protected value in both the argument
(query) and the transport header (X-Custom), while the identical
construction against HttpRequestExecutor threw and never invoked its
handler — both directions proven in the repository’s own test project.
After the fix, an 18-shape expression battery run through the real executor
— direct references, concatenation, string interpolation, record and table
literals, index access, lambdas, field access, With bindings — blocks
every leak-capable shape (16/16 executable), while the pre-fix arm forwards
them with the secret inside: the guard is the exact delta. The same
differential was re-run against the published 1.24.0 package before this
writeup went up — the former leak case now throws Cannot use a protected
value in MCP tool invocation argument 'query'., and the vendor’s own
sensitive-value tests pass.
Impact
Scope, stated plainly: the value is transmitted to an MCP server the
workflow itself selects — an author-chosen destination, over the network.
No cross-tenant reads are claimed. The precondition is a deployment that
treats workflow authorship as a lower-trust role than environment/secret
configuration — the separation this control exists to implement. CVSS 3.1 on
that reading is AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N = 7.5.
Microsoft’s servicing review assessed that a declarative workflow definition is treated as trusted input — the author is considered to be operating inside the application’s trust boundary — and that the sensitivity handling on this tool-invocation path is a defense-in-depth measure rather than a security guarantee, so the case was not serviced as a vulnerability. The engineering fix and the advisory landed through the project’s own process regardless; read this post as a coverage study of the guard family, not as a serviced CVE.
The fix
PR #8832 adds a single checked accessor and routes all five read sites through it:
private T GetInvocationValue<T>(EvaluationResult<T> result, string location)
{
if (result.Sensitivity == SensitivityLevel.Sensitive)
{
throw this.Exception($"Cannot use a protected value in MCP tool invocation {location}.");
}
return result.Value;
}
applied to the server URL, server label, tool name, every argument
(argument '{key}') and every transport header (header '{key}') — in both
direct and approval-based execution, with regression tests over all five
locations across the two execution modes.
Timeline
- 2026-09-23 — protected-value guard introduced across the original seven
executors (commit
0bbb7245d4). - 2026-09-28 — HTTP request executor gate lands (PR #8797, commit
0d6d2bb6b0); the MCP tool executor gap is reported to Microsoft the same day. - 2026-09-29 — fixed upstream: PR #8832 (commit
ba320332d4). - 2026-10-07 — advisory GHSA-9f26-7mqc-9vc4 published (High; no CVE); fix
released in
Microsoft.Agents.AI.Workflows.Declarative1.24.0 and re-verified against the published package.
Disclosure
The finding is public as GHSA-9f26-7mqc-9vc4
(published 2026-10-07; High; no CVE), the fix as PR #8832
(commit ba320332d4). This writeup describes behavior verified against the
repository at the pre-fix (f47c3782) and fixed (ba320332d4) revisions,
and re-verified against the published 1.24.0 package.