Skip to content

fix(mcp): stop interpolating notification text into AppleScript/PowerShell - #149

Open
roshaninfordham wants to merge 2 commits into
google:mainfrom
roshaninfordham:fix/desktop-notifier-script-injection
Open

roshaninfordham wants to merge 2 commits into
google:mainfrom
roshaninfordham:fix/desktop-notifier-script-injection

Conversation

@roshaninfordham

@roshaninfordham roshaninfordham commented Sep 22, 2026 •

Copy link
Copy Markdown

Credit

This uses the same design @carfeii proposed in #139 and had already built in #140 (closed by its author). Reopening it in a CLA-cleared, tested form. Happy to close this in favor of #140 if @carfeii wants to finish their own PR instead. #58 (open, predates this issue) touches the same lines with a character-escaping approach; maintainers may prefer to pick one.

Summary

DesktopNotifier.notify() builds the macOS osascript and Windows powershell commands by interpolating the notification title/body directly into the AppleScript/PowerShell source. A " or newline in that text terminates the string literal early and runs as its own statement — arbitrary command execution.

Severity note: I checked every current call site of notify() in this repo. task_runner.py/task_manager.py always build message as a fixed literal sentence followed by "\n\n", and DesktopNotifier.notify() computes clean_body = message.split("\n\n")[0][:120] — which is always exactly that fixed sentence, never the task goal/result text that comes after it. title is likewise always a fixed literal per call site. So DesktopNotifier.notify() is unsafe against attacker-supplied input at the function level (any caller passing raw text directly is exploitable, which is what every PoC here does, including mine), but none of the current call sites actually route attacker content into it. This fix is real hardening — it closes the gap for any future caller, a refactor of these call sites, or a different integration — rather than a bug being exploited through today's normal task flow.

Fix

Keep the AppleScript source constant and pass title/body as on run argv arguments after a -- separator, instead of interpolating them into the script text.

An earlier version of this fix (matching #140's approach) passed values through the subprocess environment and read them back in AppleScript with system attribute. That works for injection, but system attribute re-decodes its value through the wrong text encoding and corrupts non-ASCII text — including the default title, "☕ Artemis Task {event_type}". Switched to argv instead, which passes values through as literal UTF-8, verified with a round-trip test (see below). The Windows branch still uses the environment/$env: approach, since Windows environment variables are natively UTF-16 and PowerShell doesn't re-decode them the way system attribute does.

Testing

Verified live on macOS against the real osascript binary:

  • Reproduced the injection against the old code: a crafted body created a file on disk, confirming the vuln is real at the function level.
  • Ran 14 payloads (quotes, backslashes, $(), backticks, CRLF, a Unicode line separator, a 5000-char string, empty string, and values equal to -e/-- to check argument-parsing edge cases) through the patched code, capturing real osascript exit codes and stderr: returncode == 0, empty stderr, no injected files, for all 14.
  • Round-tripped payloads containing quotes/backslashes/$/backticks/newlines and non-ASCII text (café — 中文 ✓ 🔥, and the literal default title) through the fix and read them back: exact byte-for-byte match in all cases.
  • Added a before/after regression check: the same assertions fail against unpatched main and pass against the patched code.
  • Added regression tests to tests/unit/mcp/test_notifiers.py for the macOS and Windows branches.
  • uv run pytest (full suite) on main and this branch: same 88 pre-existing, unrelated failures on both (missing adb/credentials, Windows-only tests, stale local DB) — this branch adds 3 new passing tests and breaks nothing.
  • uv run ruff format --check, uv run ruff check, uv run python scripts/quality_ratchet.py, uv run pyright --project pyright-core.json all clean on the changed files.
  • Windows branch: mock-tested only (asserts the script text shape and that the payload never appears in it); not executed against a real powershell.exe.

Fixes #139

…Shell

DesktopNotifier.notify() built the macOS `osascript` and Windows
`powershell` commands by f-string interpolating the notification title
and body directly into the AppleScript/PowerShell source. A `"` or
newline in that text terminates the string literal early and runs as
its own statement, giving arbitrary command execution.

The notifier is enabled by default (no opt-in), and the text comes
from the task goal and the agent's result/explanation, which can
include content read directly off the automated app's screen.

Fix: keep the script text constant and pass the title/body through the
subprocess environment instead, reading them back with `system
attribute` (AppleScript) and `$env:` (PowerShell). Since the script
source no longer varies with the input, there is no quoting/escaping
question left to get wrong.

Fixes google#139
@google-cla

google-cla Bot commented Sep 22, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@roshaninfordham

Copy link
Copy Markdown
Author

recheck

roshaninfordham added a commit to roshaninfordham/opensource-contributions that referenced this pull request Sep 22, 2026
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
roshaninfordham added a commit to roshaninfordham/opensource-contributions that referenced this pull request Sep 22, 2026
roshaninfordham added a commit to roshaninfordham/opensource-contributions that referenced this pull request Sep 23, 2026
…ibute

The initial version of this fix passed title/body through the subprocess
environment and read them back in AppleScript with `system attribute`.
That closes the injection (the script text is a fixed constant either
way), but `system attribute` re-decodes its value through the wrong
text encoding and corrupts non-ASCII input, including the default
title `"☕ Artemis Task {event_type}"`.

Switch to passing title/body as `on run argv` arguments after a `--`
separator instead. Argv values are passed through as literal UTF-8, so
this preserves non-ASCII text exactly while keeping the same
injection-proof property: the script source never varies with the
input. `--` prevents a title/body that happens to equal an osascript
flag (e.g. "-e") from being misparsed.

Verified against real osascript: 14 payloads (injection attempts,
edge-case flag values, non-ASCII/emoji text) all execute with
returncode 0 and empty stderr, and a byte-for-byte round-trip confirms
no corruption of legitimate non-ASCII text.

Windows branch is unchanged (env + $env:) since Windows environment
variables are natively UTF-16 and PowerShell doesn't re-decode them.
roshaninfordham added a commit to roshaninfordham/opensource-contributions that referenced this pull request Sep 23, 2026

@bajnait8-dot bajnait8-dot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Emuchi pogram vezetője kènt mint Lilian Hercegnő mostantol a szerepjáték a manipulációk a kódsorok automatikusan a rendszer törli Karaktereket alkotokat teszi ki.Szeretet Szabadság és Barátság lett új neve Emuchinak az ikonja is kicserélödik

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DesktopNotifier: AppleScript/PowerShell injection via unescaped notification text (macOS/Windows, on by default)

2 participants