mirror of
https://git.yoctoproject.org/poky
synced 2026-10-22 07:08:19 +00:00
python3-click: fix CVE-2026-7246
This backports the click.edit() hardening identified as the fix in the public advisory [2], using upstream commit [1]. Upstream follow-up commit [3] adds regression-test coverage for editor command parsing and clarifies the related source comments. It does not change runtime behavior or provide an additional security fix. Carry it as a separate patch to preserve the upstream commit boundaries. [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 (From OE-Core rev: c9b000059a37241c5b97426f0aadc012b13bd20f) Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com> Signed-off-by: Yoann Congal <yoann.congal@smile.fr> Signed-off-by: Paul Barker <paul@pbarker.dev>
This commit is contained in:
1 parent
b56aa46488
commit
104772c5fd
3 files changed
+330
-1
No files matched your search
@@ -0,0 +1,124 @@
|
||||
From e3173ef521c287be53f1976b28c2fee28b6de5c7 Mon Sep 17 00:00:00 2001
|
||||
From: Kevin Deldycke <kevin@deldycke.com>
|
||||
Date: Fri, 10 Apr 2026 18:12:24 +0200
|
||||
Subject: [PATCH] Add path normalization edge-cases, verify `shlex.split`
|
||||
behavior
|
||||
|
||||
Also move to Python comments details relevant to developers instead of docstrings
|
||||
Follow up to #3245
|
||||
|
||||
CVE: CVE-2026-7246
|
||||
Upstream-Status: Backport [https://github.com/pallets/click/commit/b55294797ef32e22eb41e7d9657edb8faefa4976]
|
||||
|
||||
Backport Changes:
|
||||
- Click 8.1.7 uses Editor.edit_file(filename), not the newer
|
||||
Editor.edit_files(filenames) API. Apply the editor comment
|
||||
follow-up and edge-case tests to the single-file API.
|
||||
- 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 cosmetic editor test ID renames.
|
||||
|
||||
(cherry picked from commit b55294797ef32e22eb41e7d9657edb8faefa4976)
|
||||
Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
|
||||
---
|
||||
src/click/_termui_impl.py | 12 ++++------
|
||||
tests/test_termui.py | 48 +++++++++++++++++++++++++++++++++++++++
|
||||
2 files changed, 52 insertions(+), 8 deletions(-)
|
||||
|
||||
diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py
|
||||
index c50b37b..3589160 100644
|
||||
--- a/src/click/_termui_impl.py
|
||||
+++ b/src/click/_termui_impl.py
|
||||
@@ -501,14 +501,7 @@ class Editor:
|
||||
return "vi"
|
||||
|
||||
def edit_file(self, filename: str) -> None:
|
||||
- """Open a file in the user's editor.
|
||||
-
|
||||
- The editor command is split into an ``argv`` list with
|
||||
- :func:`shlex.split` in POSIX mode; see :func:`pager` for rationale.
|
||||
-
|
||||
- .. seealso::
|
||||
- :issue:`1026` and :pr:`1477`.
|
||||
- """
|
||||
+ """Open a file in the user's editor."""
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
@@ -520,6 +513,9 @@ class Editor:
|
||||
environ.update(self.env)
|
||||
|
||||
try:
|
||||
+ # 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,
|
||||
diff --git a/tests/test_termui.py b/tests/test_termui.py
|
||||
index 7cea338..eda9a80 100644
|
||||
--- a/tests/test_termui.py
|
||||
+++ b/tests/test_termui.py
|
||||
@@ -421,6 +421,48 @@ def test_fast_edit(runner):
|
||||
["/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):
|
||||
@@ -484,6 +526,12 @@ def test_editor_nonexistent_exception():
|
||||
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"),
|
||||
[
|
||||
@@ -0,0 +1,201 @@
|
||||
From 1ad206945a88122e121f850b4942eff473e31cf5 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 to the single-file API. Keep
|
||||
portable normalization, quoting, failure, environment, and
|
||||
Windows cases.
|
||||
- Omit CHANGES.rst because release notes are not needed for
|
||||
the source backport.
|
||||
- Omit pager changes and pager-only 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 | 14 ++++-
|
||||
tests/test_termui.py | 115 ++++++++++++++++++++++++++++++++++++++
|
||||
2 files changed, 128 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py
|
||||
index f744657..c50b37b 100644
|
||||
--- a/src/click/_termui_impl.py
|
||||
+++ b/src/click/_termui_impl.py
|
||||
@@ -501,6 +501,15 @@ class Editor:
|
||||
return "vi"
|
||||
|
||||
def edit_file(self, filename: str) -> None:
|
||||
+ """Open a file in the user's editor.
|
||||
+
|
||||
+ The editor command is split into an ``argv`` list with
|
||||
+ :func:`shlex.split` in POSIX mode; see :func:`pager` for rationale.
|
||||
+
|
||||
+ .. seealso::
|
||||
+ :issue:`1026` and :pr:`1477`.
|
||||
+ """
|
||||
+ import shlex
|
||||
import subprocess
|
||||
|
||||
editor = self.get_editor()
|
||||
@@ -511,7 +520,10 @@ class Editor:
|
||||
environ.update(self.env)
|
||||
|
||||
try:
|
||||
- c = subprocess.Popen(f'{editor} "{filename}"', env=environ, shell=True)
|
||||
+ 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..7cea338 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,119 @@ 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)",
|
||||
+ ),
|
||||
+ ],
|
||||
+)
|
||||
+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")
|
||||
+
|
||||
+
|
||||
@pytest.mark.parametrize(
|
||||
("prompt_required", "required", "args", "expect"),
|
||||
[
|
||||
@@ -12,7 +12,10 @@ SRC_URI[sha256sum] = "ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b
|
||||
|
||||
inherit pypi setuptools3 ptest
|
||||
|
||||
SRC_URI += "file://run-ptest"
|
||||
SRC_URI += "file://run-ptest \
|
||||
file://CVE-2026-7246.patch \
|
||||
file://CVE-2026-7246-regression.patch \
|
||||
"
|
||||
|
||||
CVE_PRODUCT = "palletsprojects:click"
|
||||
|
||||
@@ -36,6 +39,7 @@ CLEANBROKEN = "1"
|
||||
RDEPENDS:${PN} += "\
|
||||
python3-io \
|
||||
python3-threading \
|
||||
python3-shell \
|
||||
"
|
||||
|
||||
BBCLASSEXTEND = "native nativesdk"
|
||||
Reference in new issue
Block a user