From c70ee10996ba5b42a5a1807ad0ae1d562f447e1b Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 31 Mar 2026 09:14:40 +0200 Subject: [PATCH 1/4] Fix webbrowser ``%action` check bypass --- Lib/test/test_webbrowser.py | 5 +++++ Lib/webbrowser.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py index d5bb1400d2717a..59afc029a24004 100644 --- a/Lib/test/test_webbrowser.py +++ b/Lib/test/test_webbrowser.py @@ -116,6 +116,11 @@ def test_open_bad_new_parameter(self): arguments=[URL], kw=dict(new=999)) + def test_reject_action_dash_prefixes(self): + browser = self.browser_class(name=CMD_NAME) + with self.assertRaises(ValueError): + browser.open('%action--incognito') + class EdgeCommandTest(CommandTestMixin, unittest.TestCase): diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py index 9ead2990e818e5..7b30d5f62667b3 100644 --- a/Lib/webbrowser.py +++ b/Lib/webbrowser.py @@ -274,7 +274,6 @@ def _invoke(self, args, remote, autoraise, url=None): def open(self, url, new=0, autoraise=True): sys.audit("webbrowser.open", url) - self._check_url(url) if new == 0: action = self.remote_action elif new == 1: @@ -288,6 +287,8 @@ def open(self, url, new=0, autoraise=True): raise Error("Bad 'new' parameter to open(); " f"expected 0, 1, or 2, got {new}") + self._check_url(url.replace("%action", action)) + args = [arg.replace("%s", url).replace("%action", action) for arg in self.remote_args] args = [arg for arg in args if arg] From 2726f43fe8bbb1aa1b9b80f9500def077f57dab3 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 31 Mar 2026 09:15:58 +0200 Subject: [PATCH 2/4] Blurb it --- .../Security/2026-03-31-09-15-51.gh-issue-111111.EZJzz2.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-111111.EZJzz2.rst diff --git a/Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-111111.EZJzz2.rst b/Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-111111.EZJzz2.rst new file mode 100644 index 00000000000000..45cdeebe1b6d64 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-111111.EZJzz2.rst @@ -0,0 +1,2 @@ +A bypass in :mod:`webbrowser` allowed URLs prefixed with ``%action`` to pass +the dash-prefix safety check. From 69439fda46db6f7694f24d29307795ba1c736ef6 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Mon, 6 Apr 2026 16:13:04 +0100 Subject: [PATCH 3/4] Update issue number --- ....EZJzz2.rst => 2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Misc/NEWS.d/next/Security/{2026-03-31-09-15-51.gh-issue-111111.EZJzz2.rst => 2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst} (100%) diff --git a/Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-111111.EZJzz2.rst b/Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst similarity index 100% rename from Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-111111.EZJzz2.rst rename to Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst From 81ba4d5c8fbd4f078bb20ddf5ecde82f815bffba Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Mon, 6 Apr 2026 18:34:20 +0100 Subject: [PATCH 4/4] Greg's suggestions --- Lib/test/test_webbrowser.py | 4 ++++ Lib/webbrowser.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py index 59afc029a24004..d08e91d4069bfd 100644 --- a/Lib/test/test_webbrowser.py +++ b/Lib/test/test_webbrowser.py @@ -120,6 +120,10 @@ def test_reject_action_dash_prefixes(self): browser = self.browser_class(name=CMD_NAME) with self.assertRaises(ValueError): browser.open('%action--incognito') + # new=1: action is "--new-window", so "%action" itself expands to + # a dash-prefixed flag even with no dash in the original URL. + with self.assertRaises(ValueError): + browser.open('%action', new=1) class EdgeCommandTest(CommandTestMixin, unittest.TestCase): diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py index 7b30d5f62667b3..52cd91d3da206d 100644 --- a/Lib/webbrowser.py +++ b/Lib/webbrowser.py @@ -289,7 +289,7 @@ def open(self, url, new=0, autoraise=True): self._check_url(url.replace("%action", action)) - args = [arg.replace("%s", url).replace("%action", action) + args = [arg.replace("%action", action).replace("%s", url) for arg in self.remote_args] args = [arg for arg in args if arg] success = self._invoke(args, True, autoraise, url)