gh-137586: Replace 'osascript' with 'open' on macOS in webbrowser#146439
gh-137586: Replace 'osascript' with 'open' on macOS in webbrowser#146439secengjeff wants to merge 8 commits intopython:mainfrom
Conversation
…ate MacOSXOSAScript
Add a new MacOSX class that opens URLs via subprocess.run(['/usr/bin/open', ...])
instead of piping AppleScript to osascript. For named browsers, /usr/bin/open -a
<name> is used; for the default browser, /usr/bin/open <url> defers directly to
the OS URL handler.
MacOSXOSAScript is deprecated with a DeprecationWarning pointing users to MacOSX.
register_standard_browsers() is updated to use MacOSX for all macOS registrations.
osascript is a general-purpose scripting interpreter that is routinely blocked on
managed endpoints due to its abuse potential, causing webbrowser.open() to fail
silently. /usr/bin/open is Apple's purpose-built URL-opening primitive and carries
no such restrictions. This also eliminates the PATH-injection vector in the existing
os.popen("osascript", "w") call.
…pt deprecation Add MacOSXTest covering default browser open, named browser open, and failure case (non-zero returncode). Add MacOSXOSAScriptDeprecationTest verifying that instantiating MacOSXOSAScript emits a DeprecationWarning. All tests mock subprocess.run.
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
…press Sphinx lookup
- Add test_default to MacOSXTest asserting webbrowser.get() returns MacOSX - Remove test_default from MacOSXOSAScriptTest (no longer the registered default) - Suppress DeprecationWarning in MacOSXOSAScriptTest setUp and test_explicit_browser using warnings.catch_warnings() so tests for OSAScript behaviour still run cleanly - Add warnings import
…ia OS handler For non-http(s) URLs (e.g. file://), /usr/bin/open dispatches via the OS file handler, which would launch an .app bundle rather than open it in a browser. Fix this by routing non-http(s) URLs through the browser explicitly using /usr/bin/open -b <bundle-id>. Named browsers use a static bundle ID map (Chrome, Firefox, Safari, Chromium, Opera, Edge). Unknown named browsers fall back to -a. For the default browser, the bundle ID is resolved at runtime via the Objective-C runtime using NSWorkspace.URLForApplicationToOpenURL, the same lookup MacOSXOSAScript performed via AppleScript. Falls back to direct open if ctypes is unavailable. http/https URLs with the default browser continue to use /usr/bin/open directly, as macOS always routes these to the registered browser.
…ult_browser_bundle_id NSWorkspace is an AppKit class and is not registered in the ObjC runtime until AppKit is loaded. Without the explicit LoadLibrary call, objc_getClass returns nil for NSWorkspace, causing the entire lookup to silently fall back to /usr/bin/open without -b.
| (4) | ||
| Only on iOS. | ||
|
|
||
| .. deprecated:: 3.14 |
There was a problem hiding this comment.
| .. deprecated:: 3.14 | |
| .. deprecated:: next |
And precede this deprecated block with a versionadded for the new class, and move both after versionchanged:: 3.13 below.
There was a problem hiding this comment.
Also the MacOSXOSAScripts in the table above need updating, and should we also add chrome and firefox (with note (3)) as also returning the new class?
ronaldoussoren
left a comment
There was a problem hiding this comment.
I'm not sure about this PR.
The direct security issue can be fixed by changing the invocation of osascript to /usr/bin/osascript. Blocking osascript while allowing usage of Python is IMHO security theatre.
On modernist systems (macOS 10.15 or later) it is probably possible to just use NSWorkspace directly: it has all the moving peaces to implement what we need for webbrowser.open although I haven't thought through the implications of doing this yet. In particular, -[NSWorkpace openURLs:withApplicationAtURL:configuration:completionHandler:] is asynchronous which makes it harder to report errors.
| # | ||
|
|
||
| if sys.platform == 'darwin': | ||
| def _macos_default_browser_bundle_id(): |
There was a problem hiding this comment.
This function leaks memory due to not performing objective-c reference count updates.
Also: This introduces a new dependency on an Apple system framework, which in the past has caused problems for folks starting new worker processes using os.fork (without exec-in a new executable).
|
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
| (4) | ||
| Only on iOS. | ||
|
|
||
| .. deprecated:: 3.14 |
There was a problem hiding this comment.
| .. deprecated:: 3.14 | |
| .. deprecated:: next |
And precede this deprecated block with a versionadded for the new class, and move both after versionchanged:: 3.13 below.
| except Exception: | ||
| return None | ||
|
|
||
| class MacOSX(BaseBrowser): |
There was a problem hiding this comment.
It's been macOS for 10 years, shall we use drop the X for the new class?
| class MacOSX(BaseBrowser): | |
| class MacOS(BaseBrowser): |
| to -a. | ||
| """ | ||
|
|
||
| _BUNDLE_IDS = { |
There was a problem hiding this comment.
Use a frozendict if not backporting?
| (4) | ||
| Only on iOS. | ||
|
|
||
| .. deprecated:: 3.14 |
There was a problem hiding this comment.
Also the MacOSXOSAScripts in the table above need updating, and should we also add chrome and firefox (with note (3)) as also returning the new class?
| 'firefox': 'org.mozilla.firefox', | ||
| 'safari': 'com.apple.Safari', | ||
| 'chromium': 'org.chromium.Chromium', | ||
| 'opera': 'com.operasoftware.Opera', |
There was a problem hiding this comment.
I have Opera installed on macOS, and confirm this is the correct bundle ID in the plist, but whilst this works:
import webbrowser
web = webbrowser.get("firefox")
web.open("https://www.python.org/")This doesn't:
import webbrowser
web = webbrowser.get("opera")
web.open("https://www.python.org/")Traceback (most recent call last):
File "/Users/hugo/github/python/cpython/main/1.py", line 2, in <module>
web = webbrowser.get("opera")
File "/Users/hugo/github/python/cpython/main/Lib/webbrowser.py", line 68, in get
raise Error("could not locate runnable browser")
webbrowser.Error: could not locate runnable browserDo these new ones need registering too, or removing from here?
Replaces
MacOSXOSAScript, which pipes AppleScript toosascript, with a newMacOSXclass that calls/usr/bin/opendirectly viasubprocess.run.MacOSXOSAScriptis deprecated with aDeprecationWarningpointing users toMacOSX.Why
osascriptis a general-purpose AppleScript interpreter and a known LOObin (Living Off the Land binary). Because it can execute arbitrary code, it is routinely blocked by endpoint security tooling on managed Macs. This causeswebbrowser.open()to break for users of any Python application that depends on thewebbrowserlibrary, with no obvious connection toosascriptas the cause./usr/bin/openis Apple's purpose-built URL-opening primitive. It passes the URL directly to the OS-registered URL handler with no scripting interpreter involved, and is not subject to the same endpoint security restrictions.Security
This fixes gh-137586. The existing
os.popen("osascript", "w")call resolves viaPATH, creating a PATH-injection vector. The open PR for that issue (#137584) proposes switching to/usr/bin/osascript; this change eliminates the dependency entirely.File injection safety
/usr/bin/open <url>dispatches via the OS file handler, which means afile://URL pointing to an.appbundle or installer would launch it rather than open it in a browser. To prevent this, non-http(s) URLs are routed through the browser explicitly using/usr/bin/open -b <bundle-id>, ensuring the URL is always handled by a browser regardless of scheme.Named browsers use a static bundle ID map for common browsers (
com.google.Chrome,org.mozilla.firefox,com.apple.Safari, etc.). Unknown named browsers fall back to-a. For the default browser, the bundle ID is resolved at runtime via the Objective-C runtime using the sameNSWorkspace.URLForApplicationToOpenURLlookup thatMacOSXOSAScriptperformed via AppleScript, with a graceful fallback to directopenif ctypes is unavailable.Named browser support
MacOSXOSAScriptusedtell application "<name>"to target specific browsers.MacOSXpreserves this with/usr/bin/open -b <bundle-id>for known browsers and/usr/bin/open -a <name>for others.Testing
Tested locally on macOS with the default browser and named browsers (Safari, Chrome). Unit tests added covering: default http/https open, non-http URL bundle ID routing, bundle ID lookup fallback, named browser with known bundle ID, named browser fallback to
-a, failure case, and the deprecation warning.Changes
Lib/webbrowser.py: add_macos_default_browser_bundle_id(), addMacOSXwith bundle ID map, deprecateMacOSXOSAScript, updateregister_standard_browsers()Lib/test/test_webbrowser.py: addMacOSXTestandMacOSXOSAScriptDeprecationTestDoc/library/webbrowser.rst: add.. deprecated:: 3.14entry forMacOSXOSAScriptFixes gh-137586.