diff mbox series

[scarthgap] python3-click: fix CVE-2026-7246

Message ID 20260821163206.882233-1-dkelaiya@cisco.com
State New
Headers show
Series [scarthgap] python3-click: fix CVE-2026-7246 | expand

Commit Message

From: Darsh Kelaiya <dkelaiya@cisco.com>

This patch applies the upstream fix for CVE-2026-7246 as referenced
in [2], using the upstream commit identified in [1].

The backport also adapts editor regression tests from the upstream
test and documentation follow-up identified in [3]. This follow-up
does not contain an additional production security fix.

[1] https://github.com/pallets/click/commit/b96c2601af4e01341b4d2c0db494ebee4aef8f42
[2] https://github.com/tsigouris007/security-advisories/security/advisories/GHSA-47fr-3ffg-hgmw
[3] https://github.com/pallets/click/commit/b55294797ef32e22eb41e7d9657edb8faefa4976

Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
---
 .../python/python3-click/CVE-2026-7246.patch  | 245 ++++++++++++++++++
 .../python/python3-click_8.1.7.bb             |   5 +-
 2 files changed, 249 insertions(+), 1 deletion(-)
 create mode 100644 meta/recipes-devtools/python/python3-click/CVE-2026-7246.patch
diff mbox series

Patch

diff --git a/meta/recipes-devtools/python/python3-click/CVE-2026-7246.patch b/meta/recipes-devtools/python/python3-click/CVE-2026-7246.patch
new file mode 100644
index 0000000000..47ee1a551f
--- /dev/null
+++ b/meta/recipes-devtools/python/python3-click/CVE-2026-7246.patch
@@ -0,0 +1,245 @@ 
+From cb30f575b1a251e8698909bca2a443d41dad1824 Mon Sep 17 00:00:00 2001
+From: Kevin Deldycke <kevin@deldycke.com>
+Date: Wed, 4 Mar 2026 14:51:58 +0400
+Subject: [PATCH] Document and fix command string sanitizing with `shlex.split`
+
+Removes last use of `shell=True` use for command invokation for defense-in-depth.
+Refs: #1026, #1477 and #2775
+
+CVE: CVE-2026-7246
+Upstream-Status: Backport [https://github.com/pallets/click/commit/b96c2601af4e01341b4d2c0db494ebee4aef8f42]
+
+Backport Changes:
+- Click 8.1.7 uses Editor.edit_file(filename), not the newer
+  Editor.edit_files(filenames) API. Apply the argv-list change
+  to one filename without adding the multi-file API.
+- Adapt editor tests from b96c2601 and follow-up b5529479 to
+  the single-file API. Keep portable normalization, quoting,
+  failure, environment, Windows, and malformed-command cases.
+- Omit CHANGES.rst because release notes are not needed for
+  the source backport.
+- Omit pager changes and pager-only follow-up tests because
+  Click 8.1.7 uses older pager APIs and CVE-2026-7246 affects
+  click.edit(), not pager execution.
+- Omit the unrelated _translate_ch_to_exc() return cleanup.
+
+(cherry picked from commit b96c2601af4e01341b4d2c0db494ebee4aef8f42)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ src/click/_termui_impl.py |  10 ++-
+ tests/test_termui.py      | 163 ++++++++++++++++++++++++++++++++++++++
+ 2 files changed, 172 insertions(+), 1 deletion(-)
+
+diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py
+index f744657..3589160 100644
+--- a/src/click/_termui_impl.py
++++ b/src/click/_termui_impl.py
+@@ -501,6 +501,8 @@ class Editor:
+         return "vi"
+ 
+     def edit_file(self, filename: str) -> None:
++        """Open a file in the user's editor."""
++        import shlex
+         import subprocess
+ 
+         editor = self.get_editor()
+@@ -511,7 +513,13 @@ class Editor:
+             environ.update(self.env)
+ 
+         try:
+-            c = subprocess.Popen(f'{editor} "{filename}"', env=environ, shell=True)
++            # Split in POSIX mode (the default) for the same reasons as
++            # upstream pager(): strips quotes from tokens and preserves
++            # quoted Windows paths. See issue #1026 and PR #1477.
++            c = subprocess.Popen(
++                args=shlex.split(editor) + [filename],
++                env=environ,
++            )
+             exit_code = c.wait()
+             if exit_code != 0:
+                 raise ClickException(
+diff --git a/tests/test_termui.py b/tests/test_termui.py
+index 7cfa939..eda9a80 100644
+--- a/tests/test_termui.py
++++ b/tests/test_termui.py
+@@ -1,10 +1,12 @@
+ import platform
+ import time
++from unittest.mock import patch
+ 
+ import pytest
+ 
+ import click._termui_impl
+ from click._compat import WIN
++from click._termui_impl import Editor
+ 
+ 
+ class FakeClock:
+@@ -369,6 +371,167 @@ def test_fast_edit(runner):
+     assert result == "aTest\nbTest\n"
+ 
+ 
++@pytest.mark.parametrize(
++    ("editor_cmd", "filename", "expected_args"),
++    [
++        pytest.param(
++            "myeditor --wait --flag",
++            "file1.txt",
++            ["myeditor", "--wait", "--flag", "file1.txt"],
++            id="editor with args",
++        ),
++        pytest.param(
++            "vi",
++            'file"; rm -rf / ; echo "',
++            ["vi", 'file"; rm -rf / ; echo "'],
++            id="shell metacharacters in filename",
++        ),
++        # Issue #1026: editor path with spaces must be quoted.
++        pytest.param(
++            '"C:\\Program Files\\Sublime Text 3\\sublime_text.exe"',
++            "f.txt",
++            ["C:\\Program Files\\Sublime Text 3\\sublime_text.exe", "f.txt"],
++            id="quoted windows path with spaces (issue 1026)",
++        ),
++        # PR #1477: pager/editor command with flags, like ``less -FRSX``.
++        pytest.param(
++            "less -FRSX",
++            "f.txt",
++            ["less", "-FRSX", "f.txt"],
++            id="command with flags (pr 1477)",
++        ),
++        # Issue #1026: quoted command with ``--wait`` flag.
++        pytest.param(
++            '"my command" --option value arg',
++            "f.txt",
++            ["my command", "--option", "value", "arg", "f.txt"],
++            id="quoted command with args (issue 1026)",
++        ),
++        # PR #1477: unquoted Unix path.
++        pytest.param(
++            "/usr/bin/vim",
++            "f.txt",
++            ["/usr/bin/vim", "f.txt"],
++            id="unix absolute path",
++        ),
++        # Issue #1026: macOS path with escaped space.
++        pytest.param(
++            "/Applications/Sublime\\ Text.app/Contents/SharedSupport/bin/subl",
++            "f.txt",
++            ["/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl", "f.txt"],
++            id="escaped space in unix path (issue 1026)",
++        ),
++        pytest.param(
++            "  vim  ",
++            "f.txt",
++            ["vim", "f.txt"],
++            id="leading and trailing whitespace",
++        ),
++        pytest.param(
++            "vim\t--clean",
++            "f.txt",
++            ["vim", "--clean", "f.txt"],
++            id="tab-separated tokens",
++        ),
++        pytest.param(
++            "'/Applications/My Editor.app/Contents/MacOS/editor'",
++            "f.txt",
++            ["/Applications/My Editor.app/Contents/MacOS/editor", "f.txt"],
++            id="single-quoted path with spaces",
++        ),
++        pytest.param(
++            '"my editor" --wait --new-window',
++            "file 1.txt",
++            ["my editor", "--wait", "--new-window", "file 1.txt"],
++            id="quoted editor with flags and filename with spaces",
++        ),
++        pytest.param(
++            "vim -u NONE -N",
++            "f.txt",
++            ["vim", "-u", "NONE", "-N", "f.txt"],
++            id="multiple short flags",
++        ),
++        pytest.param(
++            "editor",
++            'file"name.txt',
++            ["editor", 'file"name.txt'],
++            id="filename with double quote",
++        ),
++        pytest.param(
++            "editor",
++            "file'name.txt",
++            ["editor", "file'name.txt"],
++            id="filename with single quote",
++        ),
++    ],
++)
++def test_editor_path_normalization(editor_cmd, filename, expected_args):
++    with patch("subprocess.Popen") as mock_popen:
++        mock_popen.return_value.wait.return_value = 0
++        Editor(editor=editor_cmd).edit_file(filename)
++
++        mock_popen.assert_called_once()
++        args = mock_popen.call_args[1].get("args") or mock_popen.call_args[0][0]
++        assert args == expected_args
++        assert mock_popen.call_args[1].get("shell") is None
++
++
++@pytest.mark.skipif(not WIN, reason="Windows-specific editor paths")
++@pytest.mark.parametrize(
++    ("editor_cmd", "expected_cmd"),
++    [
++        pytest.param(
++            "notepad",
++            ["notepad"],
++            id="plain notepad",
++        ),
++        pytest.param(
++            '"C:\\Program Files\\Sublime Text 3\\sublime_text.exe" --wait',
++            ["C:\\Program Files\\Sublime Text 3\\sublime_text.exe", "--wait"],
++            id="quoted path with flag",
++        ),
++    ],
++)
++def test_editor_windows_path_normalization(editor_cmd, expected_cmd):
++    """Verify that Popen receives unquoted Windows editor paths."""
++    with patch("subprocess.Popen") as mock_popen:
++        mock_popen.return_value.wait.return_value = 0
++        Editor(editor=editor_cmd).edit_file("f.txt")
++
++        args = mock_popen.call_args[1].get("args") or mock_popen.call_args[0][0]
++        assert args == expected_cmd + ["f.txt"]
++        assert mock_popen.call_args[1].get("shell") is None
++
++
++def test_editor_env_passed_through():
++    with patch("subprocess.Popen") as mock_popen:
++        mock_popen.return_value.wait.return_value = 0
++        Editor(editor="vi", env={"MY_VAR": "1"}).edit_file("f.txt")
++
++        env = mock_popen.call_args[1].get("env")
++        assert env is not None
++        assert env["MY_VAR"] == "1"
++
++
++def test_editor_failure_exception():
++    with patch("subprocess.Popen") as mock_popen:
++        mock_popen.return_value.wait.return_value = 1
++        with pytest.raises(click.ClickException, match="Editing failed"):
++            Editor(editor="vi").edit_file("f.txt")
++
++
++def test_editor_nonexistent_exception():
++    with patch("subprocess.Popen", side_effect=OSError("not found")):
++        with pytest.raises(click.ClickException, match="not found"):
++            Editor(editor="nonexistent").edit_file("f.txt")
++
++
++def test_editor_unclosed_quote():
++    """An unclosed quote in the editor command raises ValueError."""
++    with pytest.raises(ValueError, match="No closing quotation"):
++        Editor(editor='"unclosed').edit_file("f.txt")
++
++
+ @pytest.mark.parametrize(
+     ("prompt_required", "required", "args", "expect"),
+     [
diff --git a/meta/recipes-devtools/python/python3-click_8.1.7.bb b/meta/recipes-devtools/python/python3-click_8.1.7.bb
index 7d91e1af83..3c6f4df5e2 100644
--- a/meta/recipes-devtools/python/python3-click_8.1.7.bb
+++ b/meta/recipes-devtools/python/python3-click_8.1.7.bb
@@ -12,7 +12,9 @@  SRC_URI[sha256sum] = "ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b
 
 inherit pypi setuptools3 ptest
 
-SRC_URI += "file://run-ptest"
+SRC_URI += "file://run-ptest \
+            file://CVE-2026-7246.patch \
+           "
 
 RDEPENDS:${PN}-ptest += " \
 	python3-pytest \
@@ -34,6 +36,7 @@  CLEANBROKEN = "1"
 RDEPENDS:${PN} += "\
     python3-io \
     python3-threading \
+    python3-shell \
     "
 
 BBCLASSEXTEND = "native nativesdk"