This release includes 2 security fixes for security teams reviewing exposed deployments.
Topics
+14 more
Affected surfaces
ReleasePort's take
Moderate signalThe release fixes async Promise leaks in Safari navigation and corrects injection/escaping flaws across multiple Safari tooling surfaces.
Why it matters: Security fixes for Promise leaks (id 43314) and injection flaws (id 43315) with severity scores of 90 and 85 affect core testing flows; operators should upgrade to mitigate potential code‑execution vectors.
Summary
AI summaryUpdates Robustness, Injection / escaping correctness, and Tab safety & focus across a mixed release.
Changes in this release
| Type | Severity | Summary | CVE |
|---|---|---|---|
| Security | Critical |
Fixes Safari tool async waiting issues causing Promise leaks. Fixes Safari tool async waiting issues causing Promise leaks. Source: llm_adapter@2026-06-03 Confidence: low |
— |
| Security | High |
Corrects injection/escaping flaws in Safari tooling (routes, cookies, storage, style). Corrects injection/escaping flaws in Safari tooling (routes, cookies, storage, style). Source: llm_adapter@2026-06-03 Confidence: low |
— |
| Security | High |
Adds validation and sanitization to `safari_save_pdf` file‑path handling. Adds validation and sanitization to `safari_save_pdf` file‑path handling. Source: llm_adapter@2026-06-03 Confidence: low |
— |
| Bugfix | Medium |
Ensures `safari_wait_for_new_tab` registers newly opened tabs as owned. Ensures `safari_wait_for_new_tab` registers newly opened tabs as owned. Source: llm_adapter@2026-06-03 Confidence: high |
— |
| Bugfix | Medium |
Adds cleanup of temporary files for `safari_screenshot_element` and `safari_upload_file`. Adds cleanup of temporary files for `safari_screenshot_element` and `safari_upload_file`. Source: llm_adapter@2026-06-03 Confidence: high |
— |
| Bugfix | Medium |
Guards HTTP body‑size handler to avoid double `413` response and `ERR_HTTP_HEADERS_SENT`. Guards HTTP body‑size handler to avoid double `413` response and `ERR_HTTP_HEADERS_SENT`. Source: llm_adapter@2026-06-03 Confidence: high |
— |
| Bugfix | Medium |
Skips malformed `/poll` bodies in extension poll loop to avoid reconnect delays. Skips malformed `/poll` bodies in extension poll loop to avoid reconnect delays. Source: llm_adapter@2026-06-03 Confidence: high |
— |
| Bugfix | Medium |
Prevents hard reload of OAuth/redirect callback pages that lose POST data. Prevents hard reload of OAuth/redirect callback pages that lose POST data. Source: llm_adapter@2026-06-03 Confidence: high |
— |
| Bugfix | Medium |
Prevents tab‑ownership same‑origin hole that could target user’s own tabs. Prevents tab‑ownership same‑origin hole that could target user’s own tabs. Source: llm_adapter@2026-06-03 Confidence: low |
— |
| Refactor | Low |
Restricts Extension Trusted‑Types policy to only `createScript`. Restricts Extension Trusted‑Types policy to only `createScript`. Source: llm_adapter@2026-06-03 Confidence: high |
— |
Full changelog
[2.12.0] - 2026-06-02
A correctness + security pass across the engine, server, and extension.
Fixed
Async tools that never actually waited (do JavaScript can't await a Promise)
Several tools passed an async IIFE straight to AppleScript do JavaScript, which returns the moment the synchronous portion finishes — handing back an unsettled Promise ([object Promise]) instead of the result. This is the same constraint already handled for safari_evaluate/safari_wait_for; these tools now poll page state from the Node side:
safari_navigate_and_readwas fully broken — it returned[object Promise]instead of the page content. Now pollsreadyState, then reads.safari_go_back/safari_go_forwardnavigated but never updated the tracked URL, so the next operation could target the wrong tab.safari_reloadreloaded but never waited for load or refreshed the URL.safari_click_and_waitclicked but never waited;safari_fill_and_submitsubmitted but didn't wait for the result page;safari_scroll_to(text mode) scrolled once instead of looping;safari_emulate/safari_reset_emulationdidn't wait for the reload.safari_get_indexed_db/safari_list_indexed_dbsreturned[object Promise]— now routed through the async poller.safari_screenshot_element(canvas path) returned a Promise and never fell through to the reliable screencapture+crop fallback; thesafari_screenshotcanvas fallback andsafari_upload_filedrop-fallback had the same flaw.
Injection / escaping correctness
safari_mock_route: escape order was reversed ('→\'before\→\\), double-escaping quotes and breaking the injected JS. Backslash is now escaped first.safari_set_cookie/safari_set_local_storage/safari_set_session_storage/safari_delete_cookies: keys/values containing a backslash produced invalid JS (only'was escaped). Backslash is now escaped first.safari_get_computed_style: CSS property names were interpolated into injected JS unsanitized — now escaped.safari_navigate/safari_new_tab/safari_navigate_and_read: URLs were only quote-escaped — a backslash or newline could break out of the AppleScript string literal (potential AppleScript injection). Now backslash-escaped and CR/LF stripped.- Extension snapshot: page-controlled attribute values (
aria-label,title,value,href, …) are now HTML-escaped, so a crafted attribute can't inject a fakeref=/role=into the snapshot and steer the agent to the wrong element. - Extension ref lookup: attribute values are escaped and the query is guarded, so a page-controlled
aria-label/name/placeholdercontaining"no longer throws a DOMException that surfaced as a misleading "element not found".
Filesystem safety
safari_save_pdf: now validates the output path (allowlist + sensitive-path block) — it could previously overwrite any file. The Python conversion receives paths viaargvinstead of interpolating them into source (the old shell-style escaping was wrong for a Python-cstring and broke on any path containing a quote)._validateFilePath: the..check was dead (path.resolve()already strips..); it now rejects traversal in the raw input and resolves symlinks, so a symlink under/Users/pointing outside the allowlist is caught.- Temp-file leaks:
safari_screenshot_element(crop) reconstructed the wrong filename and leaked it on error;safari_upload_filenever deleted the PNG produced by image conversion. Both now clean up.
Tab safety & focus
- Tab-ownership same-origin hole: once the session opened a single tab on an origin (github.com, google.com, …), every tab on that origin — including the user's own — counted as owned, so a click/fill could land in the user's tab. The over-broad rule was removed; same-origin redirects remain covered by the path-prefix check.
safari_wait_for_new_tabnever registered the found tab as owned, so the next interaction (e.g. after an OAuth popup) was blocked by the tab-safety guard. It now tracks ownership.- Focus restore runs in
finallyinsideextensionOrFallback, so a failed operation that brought Safari to the front still hands focus back. - Clipboard restore for native paste runs in
finally— if the helper daemon dies mid-paste, the user's clipboard is restored instead of being left with the tool's text.
Robustness
- HTTP body-size guard could call
res.writeHead(413)twice on one oversized request (ERR_HTTP_HEADERS_SENT); guarded withres.headersSent. - Extension poll loop: a single malformed
/pollbody used to tear down the loop and trigger a multi-second reconnect — it's now skipped. The safety-timeout path usescontinueinstead of re-enteringpollForCommands()(no overlapping loops). - Extension navigate: sparse pages that are OAuth/redirect callbacks (
code=/token=/state=in the URL) are no longer hard-reloaded — the reload dropped POST data and could re-submit forms. safari_get_local_storage/safari_get_session_storageno longer throw on a key whose value isnull.- Window IDs are validated numeric before reaching
do shell script "screencapture -l<id>". - Stale-detection and proxy-recheck timers are
unref()'d, so they never keep the Node process alive on their own. - Extension Trusted-Types policy registers only
createScriptnow (dropped the unused, world-accessiblecreateHTML/createScriptURLpass-throughs).
Deferred (tracked, intentionally not changed here)
- The local HTTP/WebSocket bridge has no auth token — any local process can drive it. The CORS guard already blocks web pages (browsers always send an
Origin), but a no-Originlocal request passes. A proper fix needs a native-messaging handshake or a Unix-domain socket and is tracked separately, to avoid breaking the extension transport. manifest.jsonhost_permissions: ["<all_urls>"]is broader than thehttp(s)the bridge needs; narrowing it may force a permission re-grant on installed extensions, so it's deferred.
Security Fixes
- Injection/escaping fixes: backslashes escaped before quotes in `safari_mock_route`, storage setters, navigation URLs, and computed style queries; prevents AppleScript and JavaScript injection.
- Filesystem safety improvements: validated output paths for `safari_save_pdf` with allowlist and symlink resolution to block arbitrary file overwrite.
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.
Share this release
About achiya-automation/safari-mcp
Native Safari browser automation for AI agents with 80+ tools. No Chrome dependency, optimized for Apple Silicon with 60% less CPU overhead.
Related context
Related tools
Earlier breaking changes
- v2.10.5 npm audit gate now fails build on high or critical advisories.
Beta — feedback welcome: [email protected]