Skip to content

gh-137586: Replace 'osascript' with 'open' on macOS in webbrowser#146439

Open
secengjeff wants to merge 24 commits intopython:mainfrom
secengjeff:gh-137586-macosx-open
Open

gh-137586: Replace 'osascript' with 'open' on macOS in webbrowser#146439
secengjeff wants to merge 24 commits intopython:mainfrom
secengjeff:gh-137586-macosx-open

Conversation

@secengjeff
Copy link
Copy Markdown

@secengjeff secengjeff commented Mar 26, 2026

Replaces MacOSXOSAScript (which constructs and executes AppleScript via osascript) with a new MacOS class that uses /usr/bin/open. MacOSXOSAScript is deprecated and scheduled for removal in Python 3.17.

Motivation

The previous PATH-lookup vulnerability with osascript was addressed in a separate PR. However, use of osascript remains a concern:

Usability: On managed enterprise Macs, EDR/MDM tools often monitor or restrict osascript due to its role in malware campaigns. When restrictions apply, webbrowser.open() breaks with no obvious connection to osascript, making the failure difficult to diagnose.

Security: osascript is 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

  • For http/https URLs with the default browser, calls /usr/bin/open <url> directly — macOS routes these to the registered browser.
  • For all other schemes (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.
  • Default browser bundle ID is resolved from the LaunchServices plist (~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist). On fresh installs where the plist is absent, falls back to com.apple.Safari (macOS's built-in default), ensuring -b is always used.
  • Named browsers with known bundle IDs use -b; unknown names fall back to -a.

Changes

  • Lib/webbrowser.py:
    • New MacOS class using /usr/bin/open
    • _macos_default_browser_bundle_id() using plistlib (moved to top-level imports)
    • MacOSXOSAScript deprecated via warnings._deprecated(..., remove=(3, 17))
    • register("MacOSX", ...) retained as backward compatibility alias
    • Added bundle IDs for Opera, Edge, and Brave; registered all named browsers
    • Fixed builtins.open shadow: bare open() in module scope resolves to webbrowser.open(), causing infinite recursion; fixed with explicit builtins.open()
  • Lib/test/test_webbrowser.py: Tests for all new behaviour and deprecation warning
  • Doc/library/webbrowser.rst: Documentation updates, deprecated-removed:: next 3.17
  • Doc/deprecations/pending-removal-in-3.17.rst: Deprecation index entry for MacOSXOSAScript

Tested locally against Safari, Chrome, Firefox, Opera, Edge, and Brave with https, http, and file:// URLs.

References

…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.
@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Mar 26, 2026

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 skip news label instead.

@python-cla-bot
Copy link
Copy Markdown

python-cla-bot Bot commented Mar 26, 2026

All commit authors signed the Contributor License Agreement.

CLA signed

@secengjeff secengjeff changed the title gh-137586: Replace MacOSXOSAScript with MacOSX on macOS, using /usr/bin/open gh-137586: Replace 'osascript' with 'open' on macOS Mar 26, 2026
- 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.
@vstinner vstinner changed the title gh-137586: Replace 'osascript' with 'open' on macOS gh-137586: Replace 'osascript' with 'open' on macOS in webbrowser Mar 27, 2026
Comment thread Doc/library/webbrowser.rst Outdated
(4)
Only on iOS.

.. deprecated:: 3.14
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

3.15

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
.. 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

@gpshead gpshead requested a review from ned-deily April 5, 2026 22:51
@ned-deily ned-deily requested review from a team and ronaldoussoren April 6, 2026 00:19
Copy link
Copy Markdown
Contributor

@ronaldoussoren ronaldoussoren left a comment

Choose a reason for hiding this comment

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

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.

Comment thread Lib/webbrowser.py
@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Apr 6, 2026

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 I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

Comment thread Doc/library/webbrowser.rst Outdated
(4)
Only on iOS.

.. deprecated:: 3.14
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
.. 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.

Comment thread Lib/webbrowser.py Outdated
Comment thread Lib/webbrowser.py
Comment thread Doc/library/webbrowser.rst Outdated
(4)
Only on iOS.

.. deprecated:: 3.14
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Comment thread Lib/webbrowser.py
@merwok merwok added type-feature A feature request or enhancement OS-mac labels Apr 6, 2026
secengjeff and others added 4 commits April 6, 2026 09:22
…_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>
@secengjeff
Copy link
Copy Markdown
Author

I have made the requested changes; please review again

@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Apr 6, 2026

Thanks for making the requested changes!

@ronaldoussoren: please review the changes made to this pull request.

Comment thread Misc/NEWS.d/next/Library/2026-03-26-01-42-20.gh-issue-137586.KmHRwR.rst Outdated
Comment thread Misc/NEWS.d/next/Security/2026-03-26-01-42-15.gh-issue-137586.j3SkOm.rst Outdated
Comment thread Doc/library/webbrowser.rst Outdated
Comment thread Doc/library/webbrowser.rst Outdated
Comment thread Lib/webbrowser.py Outdated
Comment thread Lib/webbrowser.py Outdated
if handler.get('LSHandlerURLScheme') == 'https':
return (handler.get('LSHandlerRoleAll')
or handler.get('LSHandlerRoleViewer'))
except Exception:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can this be more specific?

Comment thread Lib/webbrowser.py
Comment thread Doc/library/webbrowser.rst Outdated
Comment thread Lib/webbrowser.py Outdated
class MacOSXOSAScript(BaseBrowser):
def __init__(self, name='default'):
import warnings
warnings.warn(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we pick a removal version, we can use warnings._deprecated():

https://discuss.python.org/t/introducing-warnings-deprecated/14856

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Switched to warnings._deprecated("webbrowser.MacOSXOSAScript", remove=(3, 17))

Comment thread Lib/webbrowser.py Outdated
Comment thread Lib/webbrowser.py
@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Apr 22, 2026

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 I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

And if you don't make the requested changes, you will be poked with soft cushions!

secengjeff and others added 7 commits April 22, 2026 19:37
…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.
@secengjeff
Copy link
Copy Markdown
Author

I have made the requested changes; please review again

@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Apr 23, 2026

Thanks for making the requested changes!

@ronaldoussoren, @gpshead: please review the changes made to this pull request.

@bedevere-app bedevere-app Bot requested a review from gpshead April 23, 2026 03:38
@secengjeff secengjeff requested a review from hugovk April 23, 2026 03:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting merge OS-mac type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants