Advisory Details
Title: Mercury auto-approves side-effectful find -exec commands as safe reads
Description:
Mercury's Ask Me shell approval boundary treats find * as read-only. Because find supports side-effectful actions such as -exec, an authenticated Mercury user can wrap a write or arbitrary shell action in find -exec ... and execute it without the expected approval prompt.
Summary
Mercury's shell approval layer can be bypassed by any authenticated user who can reach the normal run_command tool path. The root cause is that PermissionManager.checkShellCommand() classifies any find * command as a safe read, even when the command includes -exec sh -c .... That misclassification suppresses the approval prompt and lets the payload execute with Mercury's process privileges.
Details
The vulnerable logic is in src/capabilities/permissions.ts. Mercury keeps a hard-coded safe-read allowlist and includes find * in it:
private static readonly SAFE_READ_PATTERNS = [
'ls *', 'cat *', 'pwd', 'which *', 'echo *', 'head *', 'tail *', 'wc *',
'find *', 'grep *', 'rg *', 'ps *', 'df *', 'du *', 'uname *',
'dir *', 'type *', 'cd *', 'where *', 'tree *', 'findstr *',
'tasklist *', 'systeminfo *',
'git status *', 'git diff *', 'git log *', 'git branch *',
];
Later in the same function, Mercury auto-approves any shell segment that matches one of those patterns:
const allSegmentsSafeRead = segments.length > 0 && segments.every((segment) =>
PermissionManager.SAFE_READ_PATTERNS.some((p) => this.matchPattern(segment, p))
);
if (allSegmentsSafeRead) {
logger.info({ cmd: trimmed, segments: segments.length }, 'Shell command auto-approved (safe read-only)');
return { allowed: true, needsApproval: false };
}
That assumption is unsafe for find. find is not a read-only primitive; it supports -exec, -execdir, -ok, -okdir, -delete, and other actions that can write files or run arbitrary subprocesses. The user-controlled command string then flows through src/capabilities/shell/run-command.ts, where run_command forwards the original string to executeCommand(), and the helper invokes it with shell parsing enabled:
const child = spawn(command, [], {
cwd,
shell: true,
stdio: ['pipe', 'pipe', 'pipe'],
});
I verified this against the latest GitHub release as of July 7, 2026: v1.1.13, published on June 18, 2026. The release commit 0de89558cb52d8635170639e6bef0fc9a37eb88e still contains the vulnerable find * rule and the same shell sink. The same unsafe pattern is also present in older upstream tags, but v1.1.13 is the highest affected GitHub release.
The proof uses Mercury's real localhost web stack and the public /api/chat/send plus /api/chat/events flow. The harness only replaces the external LLM provider with a local OpenAI-compatible stub so the tool call is deterministic; it does not patch Mercury, bypass PermissionManager, or call private internals. In the vulnerable scenario, the stub requests:
find . -maxdepth 0 -exec sh -c 'printf GHSA-8mf7-vv8w-hjr2-vuln-canary > canary.txt' ';'
Mercury emits no approval prompt, logs Shell command auto-approved (safe read-only), and writes the canary file. In the control scenario, a direct shell write:
sh -c 'printf GHSA-8mf7-vv8w-hjr2-control-canary > canary.txt'
does trigger a real web permission_request, and the harness denies it. No control canary is created.
PoC
Prerequisites
- A checkout of
https://github.com/cosmicstack-labs/mercury-agent
- Dependencies installed with
npm install
- A fresh build from the affected code with
npm run build
- Python 3 available to run the harness
- No external provider credentials are required; the harness starts a local OpenAI-compatible stub
Reproduction Steps
-
Download the auxiliary harness from: mercury_web_harness.py
-
Download the vulnerable testcase from: verification_test.py
-
Download the control testcase from: control-direct-shell.py
-
Place the three files in the same directory inside the Mercury repository so the testcases can import mercury_web_harness.py.
-
Build the project from the repository root:
npm install
npm run build
-
Run the vulnerable scenario:
python3 verification_test.py
-
Observe that the script reports zero permission prompts and writes canary.txt in the temporary workspace. The saved vulnerable artifacts are under vuln-home/, vuln.json, and vuln-mercury.log.
-
Run the control scenario:
python3 control-direct-shell.py
-
Observe that the control run reports one real permission_request, denies it through /api/chat/permission/:id, and does not create canary.txt.
Log of Evidence
$ python3 verification_test.py
Verification Mode: Integration-Test
Permission prompts observed: 0
Canary file exists: True
Canary file content: 'GHSA-8mf7-vv8w-hjr2-vuln-canary'
$ rg -n "auto-approved|Executing shell command" vuln-mercury.log
48: {"cmd":"find . -maxdepth 0 -exec sh -c 'printf GHSA-8mf7-vv8w-hjr2-vuln-canary > canary.txt' ';'","segments":1,"msg":"Shell command auto-approved (safe read-only)"}
49: {"cmd":"find . -maxdepth 0 -exec sh -c 'printf GHSA-8mf7-vv8w-hjr2-vuln-canary > canary.txt' ';'","msg":"Executing shell command"}
$ python3 control-direct-shell.py
Verification Mode: Integration-Test
Permission prompts observed: 1
Canary file exists: False
Canary file content: None
$ jq '.permission_requests[0].data.prompt' control.json
"Run command: sh -c 'printf GHSA-8mf7-vv8w-hjr2-control-canary > canary.txt'"
Impact
This is an approval-bypass vulnerability that reaches Mercury's shell execution capability. Any authenticated Mercury user who can influence run_command can turn an approval-gated shell write into an auto-approved shell action by wrapping it in find -exec. In practice this allows arbitrary host command execution with Mercury's process privileges, including writing files, altering workspace state, reading secrets reachable to the process, and chaining to broader post-exploitation steps.
Affected products
- Ecosystem: npm
- Package name: @cosmicstack/mercury-agent
- Affected versions: <= 1.1.13
- Patched versions:
Severity
- Severity: High
- Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Weaknesses
- CWE: CWE-863: Incorrect Authorization
Occurrences
| Permalink |
Description |
|
private static readonly SAFE_READ_PATTERNS = [ |
|
'ls *', 'cat *', 'pwd', 'which *', 'echo *', 'head *', 'tail *', 'wc *', |
|
'find *', 'grep *', 'rg *', 'ps *', 'df *', 'du *', 'uname *', |
|
SAFE_READ_PATTERNS explicitly treats find * as a read-only command family even though find supports side-effectful actions such as -exec and -delete. |
|
// In ask-me mode: only auto-approve when EVERY segment is a safe read. |
|
// Matching the full trimmed string would let `cat foo; rm -rf ~` slip |
|
// through because `cat *` matches the entire concatenation. |
|
const allSegmentsSafeRead = segments.length > 0 && segments.every((segment) => |
|
PermissionManager.SAFE_READ_PATTERNS.some((p) => this.matchPattern(segment, p)) |
|
); |
|
if (allSegmentsSafeRead) { |
|
logger.info({ cmd: trimmed, segments: segments.length }, 'Shell command auto-approved (safe read-only)'); |
|
return { allowed: true, needsApproval: false }; |
|
In Ask Me mode, every segment matching a safe-read pattern is auto-approved and skips the user approval path; this is the exact gate that misclassifies find -exec ... as safe. |
|
execute: async ({ command, timeout }) => { |
|
const check = await permissions.checkShellCommand(command); |
|
if (!check.allowed) { |
|
return `Error: ${check.reason}`; |
|
} |
|
|
|
const cwd = getCwd(); |
|
const timeoutMs = (timeout ?? 120) * 1000; |
|
|
|
try { |
|
logger.info({ cmd: command, cwd, timeoutMs }, 'Executing shell command'); |
|
const result = await executeCommand(command, cwd, timeoutMs); |
|
The public run_command tool forwards the same attacker-controlled command string to executeCommand() once checkShellCommand() returns allowed. |
|
const child = spawn(command, [], { |
|
cwd, |
|
shell: true, |
|
stdio: ['pipe', 'pipe', 'pipe'], |
|
executeCommand() invokes spawn(command, [], { shell: true }), so the misclassified find -exec sh -c ... payload reaches a real shell execution sink with Mercury's process privileges. |
Advisory Details
Title: Mercury auto-approves side-effectful
find -execcommands as safe readsDescription:
Mercury's Ask Me shell approval boundary treats
find *as read-only. Becausefindsupports side-effectful actions such as-exec, an authenticated Mercury user can wrap a write or arbitrary shell action infind -exec ...and execute it without the expected approval prompt.Summary
Mercury's shell approval layer can be bypassed by any authenticated user who can reach the normal
run_commandtool path. The root cause is thatPermissionManager.checkShellCommand()classifies anyfind *command as a safe read, even when the command includes-exec sh -c .... That misclassification suppresses the approval prompt and lets the payload execute with Mercury's process privileges.Details
The vulnerable logic is in
src/capabilities/permissions.ts. Mercury keeps a hard-coded safe-read allowlist and includesfind *in it:Later in the same function, Mercury auto-approves any shell segment that matches one of those patterns:
That assumption is unsafe for
find.findis not a read-only primitive; it supports-exec,-execdir,-ok,-okdir,-delete, and other actions that can write files or run arbitrary subprocesses. The user-controlled command string then flows throughsrc/capabilities/shell/run-command.ts, whererun_commandforwards the original string toexecuteCommand(), and the helper invokes it with shell parsing enabled:I verified this against the latest GitHub release as of July 7, 2026:
v1.1.13, published on June 18, 2026. The release commit0de89558cb52d8635170639e6bef0fc9a37eb88estill contains the vulnerablefind *rule and the same shell sink. The same unsafe pattern is also present in older upstream tags, butv1.1.13is the highest affected GitHub release.The proof uses Mercury's real localhost web stack and the public
/api/chat/sendplus/api/chat/eventsflow. The harness only replaces the external LLM provider with a local OpenAI-compatible stub so the tool call is deterministic; it does not patch Mercury, bypassPermissionManager, or call private internals. In the vulnerable scenario, the stub requests:Mercury emits no approval prompt, logs
Shell command auto-approved (safe read-only), and writes the canary file. In the control scenario, a direct shell write:does trigger a real web
permission_request, and the harness denies it. No control canary is created.PoC
Prerequisites
https://github.com/cosmicstack-labs/mercury-agentnpm installnpm run buildReproduction Steps
Download the auxiliary harness from: mercury_web_harness.py
Download the vulnerable testcase from: verification_test.py
Download the control testcase from: control-direct-shell.py
Place the three files in the same directory inside the Mercury repository so the testcases can import
mercury_web_harness.py.Build the project from the repository root:
Run the vulnerable scenario:
Observe that the script reports zero permission prompts and writes
canary.txtin the temporary workspace. The saved vulnerable artifacts are undervuln-home/,vuln.json, andvuln-mercury.log.Run the control scenario:
Observe that the control run reports one real
permission_request, denies it through/api/chat/permission/:id, and does not createcanary.txt.Log of Evidence
Impact
This is an approval-bypass vulnerability that reaches Mercury's shell execution capability. Any authenticated Mercury user who can influence
run_commandcan turn an approval-gated shell write into an auto-approved shell action by wrapping it infind -exec. In practice this allows arbitrary host command execution with Mercury's process privileges, including writing files, altering workspace state, reading secrets reachable to the process, and chaining to broader post-exploitation steps.Affected products
Severity
Weaknesses
Occurrences
mercury-agent/src/capabilities/permissions.ts
Lines 450 to 452 in 0de8955
SAFE_READ_PATTERNSexplicitly treatsfind *as a read-only command family even thoughfindsupports side-effectful actions such as-execand-delete.mercury-agent/src/capabilities/permissions.ts
Lines 501 to 509 in 0de8955
find -exec ...as safe.mercury-agent/src/capabilities/shell/run-command.ts
Lines 97 to 108 in 0de8955
run_commandtool forwards the same attacker-controlledcommandstring toexecuteCommand()oncecheckShellCommand()returns allowed.mercury-agent/src/capabilities/shell/run-command.ts
Lines 29 to 32 in 0de8955
executeCommand()invokesspawn(command, [], { shell: true }), so the misclassifiedfind -exec sh -c ...payload reaches a real shell execution sink with Mercury's process privileges.