Summary
mcp_server/notifiers/desktop.py's DesktopNotifier.notify() builds the macOS and Windows toast-notification commands by f-string-interpolating the notification title/body directly into an AppleScript display notification "..." string literal (macOS) and a PowerShell CreateTextNode("...") string literal (Windows), then executes them via osascript -e / powershell -Command with no escaping. A " or newline in the notified text lets it terminate the string literal early and run as its own AppleScript/PowerShell statement.
This is reachable with no operator opt-in: DesktopNotifier.is_available() returns True by default (only ARTEMIS_DESKTOP_NOTIFY=false or CI=true disable it), so this fires after every task on any default macOS or Windows install that has osascript/powershell available (i.e. essentially every one).
Details
mcp_server/notifiers/desktop.py, notify():
elif sys.platform == "darwin":
if shutil.which("osascript"):
script = f'display notification "{clean_body}" with title "{header}"'
subprocess.run(["osascript", "-e", script], ...)
elif sys.platform == "win32":
ps_cmd = (
...
f'$textNodes.Item(0).AppendChild($template.CreateTextNode("{header}")) > $null; '
f'$textNodes.Item(1).AppendChild($template.CreateTextNode("{clean_body}")) > $null; '
...
)
subprocess.run(["powershell", "-NoProfile", "-Command", ps_cmd], ...)
clean_body is derived from the message argument (message.split("\n\n")[0][:120]), which is built in mcp_server/background/task_runner.py from the task's goal and its result — and result is the agent's own report of what it did/observed while automating a device, which can include text read directly off an app's or web page's screen (e.g. a task like "summarize the article on screen" or "read me the reviews"). An attacker who controls content the agent is instructed to read therefore controls this string.
PoC
Live-tested against the real DesktopNotifier.notify() code (with sys.platform/shutil.which/subprocess.run patched, since this sandbox has neither macOS nor Windows) using a crafted message:
payload = 'pwned" \ndo shell script "touch /tmp/artemis_pwned"\n--'
produces the following osascript -e argument, unmodified by the current code:
display notification "pwned"
do shell script "touch /tmp/artemis_pwned"
--" with title "Artemis Task Completed"
do shell script "touch /tmp/artemis_pwned" runs as its own AppleScript statement — arbitrary shell command execution as the local user. The Windows branch has the identical shape via CreateTextNode("{header}")/CreateTextNode("{clean_body}").
Impact
Local code/command execution as the user running the Artemis MCP server, triggered purely by the content the automated agent is told to read or interact with during a normal task — no separate victim action beyond running the task itself, and no notifier opt-in required since this is on by default on macOS/Windows.
Suggested fix
I have a fix and will open a PR: pass the title/body through the subprocess environment instead of interpolating them into the AppleScript/PowerShell source, and read them back with system attribute (AppleScript) / $env: (PowerShell). The script text becomes a fixed constant that no longer varies with input, so no quoting/escaping question remains. Verified with a new regression test that patches sys.platform to darwin/win32 and asserts the injected payload never appears inside the constructed script text (only in the env dict passed alongside it).
Private reported to Google as per security.md but Bug Hunter's response contradicted it:
Unfortunately, we've determined that this project falls into the OT2 or OT3 tier of our OSS VRP. As per our program rules, we are no longer accepting or rewarding product vulnerabilities and other security vulnerabilities for projects in these tiers. You're welcome to open an issue or pull request directly on the GitHub repository to address this.
Summary
mcp_server/notifiers/desktop.py'sDesktopNotifier.notify()builds the macOS and Windows toast-notification commands by f-string-interpolating the notification title/body directly into an AppleScriptdisplay notification "..."string literal (macOS) and a PowerShellCreateTextNode("...")string literal (Windows), then executes them viaosascript -e/powershell -Commandwith no escaping. A"or newline in the notified text lets it terminate the string literal early and run as its own AppleScript/PowerShell statement.This is reachable with no operator opt-in:
DesktopNotifier.is_available()returnsTrueby default (onlyARTEMIS_DESKTOP_NOTIFY=falseorCI=truedisable it), so this fires after every task on any default macOS or Windows install that hasosascript/powershellavailable (i.e. essentially every one).Details
mcp_server/notifiers/desktop.py,notify():clean_bodyis derived from themessageargument (message.split("\n\n")[0][:120]), which is built inmcp_server/background/task_runner.pyfrom the task's goal and itsresult— andresultis the agent's own report of what it did/observed while automating a device, which can include text read directly off an app's or web page's screen (e.g. a task like "summarize the article on screen" or "read me the reviews"). An attacker who controls content the agent is instructed to read therefore controls this string.PoC
Live-tested against the real
DesktopNotifier.notify()code (withsys.platform/shutil.which/subprocess.runpatched, since this sandbox has neither macOS nor Windows) using a crafted message:produces the following
osascript -eargument, unmodified by the current code:do shell script "touch /tmp/artemis_pwned"runs as its own AppleScript statement — arbitrary shell command execution as the local user. The Windows branch has the identical shape viaCreateTextNode("{header}")/CreateTextNode("{clean_body}").Impact
Local code/command execution as the user running the Artemis MCP server, triggered purely by the content the automated agent is told to read or interact with during a normal task — no separate victim action beyond running the task itself, and no notifier opt-in required since this is on by default on macOS/Windows.
Suggested fix
I have a fix and will open a PR: pass the title/body through the subprocess environment instead of interpolating them into the AppleScript/PowerShell source, and read them back with
system attribute(AppleScript) /$env:(PowerShell). The script text becomes a fixed constant that no longer varies with input, so no quoting/escaping question remains. Verified with a new regression test that patchessys.platformtodarwin/win32and asserts the injected payload never appears inside the constructed script text (only in the env dict passed alongside it).Private reported to Google as per security.md but Bug Hunter's response contradicted it: