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 24 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.
|
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.
| (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?
…_standard_browsers on macOS
These browsers were present in MacOSX._BUNDLE_IDS but not registered,
causing webbrowser.get("opera") etc. to raise Error: could not locate
runnable browser.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ib to address memory and os.fork() concerns
|
I have made the requested changes; please review again |
|
Thanks for making the requested changes! @ronaldoussoren: please review the changes made to this pull request. |
| if handler.get('LSHandlerURLScheme') == 'https': | ||
| return (handler.get('LSHandlerRoleAll') | ||
| or handler.get('LSHandlerRoleViewer')) | ||
| except Exception: |
| class MacOSXOSAScript(BaseBrowser): | ||
| def __init__(self, name='default'): | ||
| import warnings | ||
| warnings.warn( |
There was a problem hiding this comment.
If we pick a removal version, we can use warnings._deprecated():
https://discuss.python.org/t/introducing-warnings-deprecated/14856
There was a problem hiding this comment.
Switched to warnings._deprecated("webbrowser.MacOSXOSAScript", remove=(3, 17))
|
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 And if you don't make the requested changes, you will be poked with soft cushions! |
…mHRwR.rst Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
…j3SkOm.rst Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
…Brave
Return 'com.apple.Safari' instead of None when the LaunchServices plist is
absent (fresh installs never write it). None caused the non-http branch to
fall back to bare '/usr/bin/open <url>', bypassing -b and triggering the OS
file handler. Addresses gpshead's CHANGES_REQUESTED.
Fix infinite recursion: webbrowser's module-level open() shadowed
builtins.open, so open(plist, 'rb') called webbrowser.open() recursively
on every non-http/https URL. Use builtins.open() explicitly.
Narrow 'except Exception' to '(OSError, KeyError, ValueError)'. Addresses
hugovk's review comment.
Add 'brave browser': 'com.brave.Browser' to _BUNDLE_IDS and register it
so webbrowser.get('brave') works and Brave uses precise -b dispatch instead
of the name-based -a fallback.
Tested locally against Safari, Chrome, Firefox, Opera, Edge, and Brave
with https, http, and file:// URLs. 29/29 checks pass.
Move plistlib to top-level imports alongside the existing import block.
Remove os from the inline import inside _macos_default_browser_bundle_id
as it is already imported at module level. Remove the stale
'# Maintained by Georg Brandl.' comment.
Replace manual warnings.warn() in MacOSXOSAScript.__init__ with
warnings._deprecated("webbrowser.MacOSXOSAScript", remove=(3, 17)).
Change 'deprecated:: next' to 'deprecated-removed:: next 3.17' in
Doc/library/webbrowser.rst. Add MacOSXOSAScript to
Doc/deprecations/pending-removal-in-3.17.rst.
Add register("MacOSX", None, MacOS('default')) as a backward
compatibility alias so webbrowser.get("MacOSX") continues to work.
|
I have made the requested changes; please review again |
|
Thanks for making the requested changes! @ronaldoussoren, @gpshead: please review the changes made to this pull request. |
Replaces
MacOSXOSAScript(which constructs and executes AppleScript viaosascript) with a newMacOSclass that uses/usr/bin/open.MacOSXOSAScriptis deprecated and scheduled for removal in Python 3.17.Motivation
The previous PATH-lookup vulnerability with
osascriptwas addressed in a separate PR. However, use ofosascriptremains a concern:Usability: On managed enterprise Macs, EDR/MDM tools often monitor or restrict
osascriptdue to its role in malware campaigns. When restrictions apply,webbrowser.open()breaks with no obvious connection toosascript, making the failure difficult to diagnose.Security:
osascriptis a general-purpose scripting interpreter and a known Living-Off-the-Land binary. It was used on macOS in the Axios npm supply-chain attack (March 31, 2026). Opening a URL requires none of its scripting power. Using it unnecessarily increases the standard library's attack surface and keeps Python dependent on a tool security teams routinely flag.Implementation
http/httpsURLs with the default browser, calls/usr/bin/open <url>directly — macOS routes these to the registered browser.file://, etc.), uses/usr/bin/open -b <bundle-id> <url>so the URL is always passed to a browser rather than the OS file handler, preventing file injection attacks.~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist). On fresh installs where the plist is absent, falls back tocom.apple.Safari(macOS's built-in default), ensuring-bis always used.-b; unknown names fall back to-a.Changes
Lib/webbrowser.py:MacOSclass using/usr/bin/open_macos_default_browser_bundle_id()usingplistlib(moved to top-level imports)MacOSXOSAScriptdeprecated viawarnings._deprecated(..., remove=(3, 17))register("MacOSX", ...)retained as backward compatibility aliasbuiltins.openshadow: bareopen()in module scope resolves towebbrowser.open(), causing infinite recursion; fixed with explicitbuiltins.open()Lib/test/test_webbrowser.py: Tests for all new behaviour and deprecation warningDoc/library/webbrowser.rst: Documentation updates,deprecated-removed:: next 3.17Doc/deprecations/pending-removal-in-3.17.rst: Deprecation index entry forMacOSXOSAScriptTested locally against Safari, Chrome, Firefox, Opera, Edge, and Brave with
https,http, andfile://URLs.References