File CVE-2026-32274.patch of Package python-black.43167
From 4937fe6cf241139ddbfc16b0bdbb5b422798909d Mon Sep 17 00:00:00 2001
From: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Date: Wed, 11 Mar 2026 19:57:24 -0700
Subject: [PATCH] Fix some shenanigans with the cache file and IPython (#5038)
---
CHANGES.md | 7 +++++++
src/black/handle_ipynb_magics.py | 21 +++++++++++++++++----
src/black/mode.py | 7 +++----
tests/test_black.py | 9 +++++++++
tests/test_ipynb.py | 20 ++++++++++++++++++--
5 files changed, 54 insertions(+), 10 deletions(-)
Index: black-24.3.0/src/black/handle_ipynb_magics.py
===================================================================
--- black-24.3.0.orig/src/black/handle_ipynb_magics.py
+++ black-24.3.0/src/black/handle_ipynb_magics.py
@@ -4,6 +4,8 @@ import ast
import collections
import dataclasses
import secrets
+import string
+from collections.abc import Collection
import sys
from functools import lru_cache
from importlib.util import find_spec
@@ -41,7 +43,6 @@ PYTHON_CELL_MAGICS = frozenset((
"time",
"timeit",
))
-TOKEN_HEX = secrets.token_hex
@dataclasses.dataclass(frozen=True)
@@ -130,7 +131,7 @@ def mask_cell(src: str) -> Tuple[str, Li
becomes
- "25716f358c32750e"
+ b"25716f358c32750"
'foo'
The replacements are returned, along with the transformed code.
@@ -160,7 +161,26 @@ def mask_cell(src: str) -> Tuple[str, Li
return transformed, replacements
-def get_token(src: str, magic: str) -> str:
+def create_token(n_chars: int) -> str:
+ """Create a randomly generated token that is n_chars characters long."""
+ assert n_chars > 0
+ if n_chars == 1:
+ return secrets.choice(string.ascii_letters)
+ if n_chars < 4:
+ return "_" + "".join(
+ secrets.choice(string.ascii_letters + string.digits + "_")
+ for _ in range(n_chars - 1)
+ )
+ n_bytes = max(n_chars // 2 - 1, 1)
+ token = secrets.token_hex(n_bytes)
+ if len(token) + 3 > n_chars:
+ token = token[:-1]
+ # We use a bytestring so that the string does not get interpreted
+ # as a docstring.
+ return f'b"{token}"'
+
+
+def get_token(src: str, magic: str, existing_tokens: Collection[str] = ()) -> str:
"""Return randomly generated token to mask IPython magic with.
For example, if 'magic' was `%matplotlib inline`, then a possible
@@ -169,11 +189,11 @@ def get_token(src: str, magic: str) -> s
not already present anywhere else in the cell.
"""
assert magic
- nbytes = max(len(magic) // 2 - 1, 1)
- token = TOKEN_HEX(nbytes)
+ n_chars = len(magic)
+ token = create_token(n_chars)
counter = 0
- while token in src:
- token = TOKEN_HEX(nbytes)
+ while token in src or token in existing_tokens:
+ token = create_token(n_chars)
counter += 1
if counter > 100:
raise AssertionError(
@@ -181,9 +201,7 @@ def get_token(src: str, magic: str) -> s
"Please report a bug on https://github.com/psf/black/issues. "
f"The magic might be helpful: {magic}"
) from None
- if len(token) + 2 < len(magic):
- token = f"{token}."
- return f'"{token}"'
+ return token
def replace_cell_magics(src: str) -> Tuple[str, List[Replacement]]:
@@ -236,6 +254,7 @@ def replace_magics(src: str) -> Tuple[st
The replacement, along with the transformed code, are returned.
"""
replacements = []
+ existing_tokens: set[str] = set()
magic_finder = MagicFinder()
magic_finder.visit(ast.parse(src))
new_srcs = []
@@ -251,8 +270,9 @@ def replace_magics(src: str) -> Tuple[st
offsets_and_magics[0].col_offset,
offsets_and_magics[0].magic,
)
- mask = get_token(src, magic)
+ mask = get_token(src, magic, existing_tokens)
replacements.append(Replacement(mask=mask, src=magic))
+ existing_tokens.add(mask)
line = line[:col_offset] + mask
new_srcs.append(line)
return "\n".join(new_srcs), replacements
@@ -272,7 +292,9 @@ def unmask_cell(src: str, replacements:
foo = bar
"""
for replacement in replacements:
- src = src.replace(replacement.mask, replacement.src)
+ if src.count(replacement.mask) != 1:
+ raise NothingChanged
+ src = src.replace(replacement.mask, replacement.src, 1)
return src
Index: black-24.3.0/src/black/mode.py
===================================================================
--- black-24.3.0.orig/src/black/mode.py
+++ black-24.3.0/src/black/mode.py
@@ -246,10 +246,9 @@ class Mode:
+ "@"
+ ",".join(sorted(self.python_cell_magics))
)
- if len(features_and_magics) > _MAX_CACHE_KEY_PART_LENGTH:
- features_and_magics = sha256(features_and_magics.encode()).hexdigest()[
- :_MAX_CACHE_KEY_PART_LENGTH
- ]
+ features_and_magics = sha256(features_and_magics.encode()).hexdigest()[
+ :_MAX_CACHE_KEY_PART_LENGTH
+ ]
parts = [
version_str,
str(self.line_length),
Index: black-24.3.0/tests/test_black.py
===================================================================
--- black-24.3.0.orig/tests/test_black.py
+++ black-24.3.0/tests/test_black.py
@@ -2106,6 +2106,15 @@ class TestCaching:
# doesn't get too crazy.
assert len(cache_file.name) <= 96
+ def test_cache_file_path_ignores_python_cell_magic_separators(self) -> None:
+ mode = replace(DEFAULT_MODE, python_cell_magics={"../../../tmp/pwned"})
+ with cache_dir() as workspace:
+ cache_file = get_cache_file(mode)
+ assert cache_file.parent == workspace
+ assert "/" not in cache_file.name
+ assert ".." not in cache_file.name
+ assert "../../../tmp/pwned" not in mode.get_cache_key()
+
def test_cache_broken_file(self) -> None:
mode = DEFAULT_MODE
with cache_dir() as workspace:
Index: black-24.3.0/tests/test_ipynb.py
===================================================================
--- black-24.3.0.orig/tests/test_ipynb.py
+++ black-24.3.0/tests/test_ipynb.py
@@ -6,8 +6,8 @@ from dataclasses import replace
from typing import ContextManager
import pytest
-from _pytest.monkeypatch import MonkeyPatch
from click.testing import CliRunner
+from pytest import MonkeyPatch
from black import (
Mode,
@@ -17,7 +17,12 @@ from black import (
format_file_in_place,
main,
)
-from black.handle_ipynb_magics import jupyter_dependencies_are_installed
+from black.handle_ipynb_magics import (
+ Replacement,
+ create_token,
+ jupyter_dependencies_are_installed,
+ unmask_cell,
+)
from tests.util import DATA_DIR, get_case_path, read_jupyter_notebook
with contextlib.suppress(ModuleNotFoundError):
@@ -39,6 +44,17 @@ def test_noop() -> None:
format_cell(src, fast=True, mode=JUPYTER_MODE)
+@pytest.mark.parametrize("n_chars", [1, 2, 3, 4, 5, 17])
+def test_create_token_uses_requested_length(n_chars: int) -> None:
+ assert len(create_token(n_chars)) == n_chars
+
+
+def test_unmask_cell_raises_when_token_is_not_unique() -> None:
+ replacement = Replacement(mask='b"dead"', src="%time")
+ with pytest.raises(NothingChanged):
+ unmask_cell(f"{replacement.mask}\nvalue = {replacement.mask}", [replacement])
+
+
@pytest.mark.parametrize("fast", [True, False])
def test_trailing_semicolon(fast: bool) -> None:
src = 'foo = "a" ;'
@@ -509,8 +525,8 @@ def test_ipynb_and_pyi_flags() -> None:
def test_unable_to_replace_magics(monkeypatch: MonkeyPatch) -> None:
- src = "%%time\na = 'foo'"
- monkeypatch.setattr("black.handle_ipynb_magics.TOKEN_HEX", lambda _: "foo")
+ src = '%%time\na = b"foo"'
+ monkeypatch.setattr("secrets.token_hex", lambda _: "foo")
with pytest.raises(
AssertionError, match="Black was not able to replace IPython magic"
):