Summary
The samples/dotnet/A2ACliDemo/CLIServer sample upgrades remote A2A TextPart
content into a local shell command. It attempts to protect execution with an
allowlist, but the allowlist only checks the first token returned by
ParseCommand(input).
On Unix-like systems the remaining unvalidated argument string is interpolated
into:
/bin/bash -c "{command} {arguments}"
An attacker can use an allowed first token such as echo and place shell
metacharacters in the argument string. Example:
echo a2a-safe; printf A2A_PWNED_7f4d7c
ParseCommand treats echo as the command, IsCommandAllowed("echo") accepts
it, and /bin/bash -c executes the trailing printf command.
Affected Target
- Repository:
a2aproject/a2a-samples
- Commit tested:
d536d049451864995df9b5bb0b4f2e2d3cf65274
- Component:
samples/dotnet/A2ACliDemo/CLIServer/CLIAgent.cs
Security Impact
If the sample CLI agent is exposed to untrusted A2A clients, a remote client can
execute shell metacharacters after any allowlisted command token. This bypasses
the sample's stated command allowlist boundary and can become remote command
execution in deployments that reuse the sample.
This is a sample issue rather than a core SDK issue, but it is security-relevant
because the sample explicitly demonstrates a bridge from A2A messages to
system-level operations.
The repository README also warns that data received from external agents,
including messages and artifacts, should be treated as untrusted input. This
issue is a concrete example of that boundary being crossed: untrusted A2A
message text is interpreted as a shell command line.
The configured allowlist also includes interpreter and package-manager style
commands such as python, node, npm, and dotnet. Even if shell
metacharacter injection were fixed, these entries should be treated carefully
because they can execute attacker-provided program text or project scripts when
arguments remain remotely controlled.
Root Cause
The code extracts message text from the A2A request:
var userText = GetTextFromMessage(messageSendParams.Message);
It parses only the first whitespace-delimited token as the command:
var parts = ParseCommand(input);
var command = parts.Command;
var arguments = parts.Arguments;
It allowlists only command:
if (!IsCommandAllowed(command)) { ... }
But then passes both command and attacker-controlled arguments through a
shell:
process.StartInfo.Arguments = $"-c \"{command} {arguments}\"";
The guard and sink do not protect the same semantic object.
Steps to reproduce
From the extracted package directory:
cd attachments
bash reproduce.sh
The included PoC is source-anchored and does not require the .NET SDK. It checks
the vulnerable anchors in CLIAgent.cs, replays the sample's parse and allowlist
logic, and executes the resulting benign shell command locally to prove that the
trailing marker command runs.
The script clones the tested a2a-samples commit if no local checkout is
provided. To run against an existing checkout:
cd attachments
A2A_SAMPLES_TARGET_DIR=/path/to/a2a-samples bash reproduce.sh
Expected result:
{
"verdict": "fail",
"first_token_allowed": true,
"marker_observed": true
}
Suggested Fix
Avoid shell interpretation for user-controlled message text. Prefer one of:
- dispatch each allowed command to a fixed handler;
- invoke a fixed executable with an argument array and no shell;
- validate the complete command grammar, including arguments, before execution;
- remove interpreters and package managers such as
python, node, npm, and
dotnet from the default allowlist unless their arguments are separately
constrained by command-specific parsers;
- remove shell-backed execution from the public sample or mark it as trusted
local-only.
Attachments
attachments.zip
Summary
The
samples/dotnet/A2ACliDemo/CLIServersample upgrades remote A2ATextPartcontent into a local shell command. It attempts to protect execution with an
allowlist, but the allowlist only checks the first token returned by
ParseCommand(input).On Unix-like systems the remaining unvalidated argument string is interpolated
into:
An attacker can use an allowed first token such as
echoand place shellmetacharacters in the argument string. Example:
ParseCommandtreatsechoas the command,IsCommandAllowed("echo")acceptsit, and
/bin/bash -cexecutes the trailingprintfcommand.Affected Target
a2aproject/a2a-samplesd536d049451864995df9b5bb0b4f2e2d3cf65274samples/dotnet/A2ACliDemo/CLIServer/CLIAgent.csSecurity Impact
If the sample CLI agent is exposed to untrusted A2A clients, a remote client can
execute shell metacharacters after any allowlisted command token. This bypasses
the sample's stated command allowlist boundary and can become remote command
execution in deployments that reuse the sample.
This is a sample issue rather than a core SDK issue, but it is security-relevant
because the sample explicitly demonstrates a bridge from A2A messages to
system-level operations.
The repository README also warns that data received from external agents,
including messages and artifacts, should be treated as untrusted input. This
issue is a concrete example of that boundary being crossed: untrusted A2A
message text is interpreted as a shell command line.
The configured allowlist also includes interpreter and package-manager style
commands such as
python,node,npm, anddotnet. Even if shellmetacharacter injection were fixed, these entries should be treated carefully
because they can execute attacker-provided program text or project scripts when
arguments remain remotely controlled.
Root Cause
The code extracts message text from the A2A request:
It parses only the first whitespace-delimited token as the command:
It allowlists only
command:But then passes both
commandand attacker-controlledargumentsthrough ashell:
The guard and sink do not protect the same semantic object.
Steps to reproduce
From the extracted package directory:
cd attachments bash reproduce.shThe included PoC is source-anchored and does not require the .NET SDK. It checks
the vulnerable anchors in
CLIAgent.cs, replays the sample's parse and allowlistlogic, and executes the resulting benign shell command locally to prove that the
trailing marker command runs.
The script clones the tested
a2a-samplescommit if no local checkout isprovided. To run against an existing checkout:
cd attachments A2A_SAMPLES_TARGET_DIR=/path/to/a2a-samples bash reproduce.shExpected result:
{ "verdict": "fail", "first_token_allowed": true, "marker_observed": true }Suggested Fix
Avoid shell interpretation for user-controlled message text. Prefer one of:
python,node,npm, anddotnetfrom the default allowlist unless their arguments are separatelyconstrained by command-specific parsers;
local-only.
Attachments
attachments.zip