Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected], [email protected]
Control: affects -1 + src:black
User: [email protected]
Usertags: pu
* CVE-2026-32274: Arbitrary file writes from unsanitized user input
(Closes: #1130657)
diffstat for black-25.1.0 black-25.1.0
changelog | 8
patches/0001-Fix-some-shenanigans-with-the-cache-file-and-IPython.patch | 175
++++++++++
patches/series | 1
3 files changed, 184 insertions(+)
diff -Nru black-25.1.0/debian/changelog black-25.1.0/debian/changelog
--- black-25.1.0/debian/changelog 2025-05-27 16:41:12.000000000 +0300
+++ black-25.1.0/debian/changelog 2026-05-06 12:45:09.000000000 +0300
@@ -1,3 +1,11 @@
+black (25.1.0-3+deb13u1) trixie; urgency=medium
+
+ * Non-maintainer upload.
+ * CVE-2026-32274: Arbitrary file writes from unsanitized user input
+ (Closes: #1130657)
+
+ -- Adrian Bunk <[email protected]> Wed, 06 May 2026 12:45:09 +0300
+
black (25.1.0-3) unstable; urgency=medium
* Team upload
diff -Nru
black-25.1.0/debian/patches/0001-Fix-some-shenanigans-with-the-cache-file-and-IPython.patch
black-25.1.0/debian/patches/0001-Fix-some-shenanigans-with-the-cache-file-and-IPython.patch
---
black-25.1.0/debian/patches/0001-Fix-some-shenanigans-with-the-cache-file-and-IPython.patch
1970-01-01 02:00:00.000000000 +0200
+++
black-25.1.0/debian/patches/0001-Fix-some-shenanigans-with-the-cache-file-and-IPython.patch
2026-05-06 12:44:42.000000000 +0300
@@ -0,0 +1,175 @@
+From d3a56c22dc6273dc9d7456b77b6e1f6cb5141b85 Mon Sep 17 00:00:00 2001
+From: Jelle Zijlstra <[email protected]>
+Date: Wed, 11 Mar 2026 19:57:24 -0700
+Subject: Fix some shenanigans with the cache file and IPython (#5038)
+
+---
+ src/black/handle_ipynb_magics.py | 21 +++++++++++++++++----
+ src/black/mode.py | 7 +++----
+ tests/test_black.py | 9 +++++++++
+ tests/test_ipynb.py | 20 ++++++++++++++++++--
+ 4 files changed, 47 insertions(+), 10 deletions(-)
+
+diff --git a/src/black/handle_ipynb_magics.py
b/src/black/handle_ipynb_magics.py
+index dd680bf..1c6ea4e 100644
+--- a/src/black/handle_ipynb_magics.py
++++ b/src/black/handle_ipynb_magics.py
+@@ -5,7 +5,9 @@
+ import dataclasses
+ import re
+ import secrets
++import string
+ import sys
++from collections.abc import Collection
+ from functools import lru_cache
+ from importlib.util import find_spec
+ from typing import Optional
+@@ -194,6 +196,13 @@ def mask_cell(src: str) -> tuple[str, list[Replacement]]:
+ 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:
+@@ -203,7 +212,7 @@ def create_token(n_chars: int) -> str:
+ return f'b"{token}"'
+
+
+-def get_token(src: str, magic: str) -> str:
++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
+@@ -215,7 +224,7 @@ def get_token(src: str, magic: str) -> str:
+ n_chars = len(magic)
+ token = create_token(n_chars)
+ counter = 0
+- while token in src:
++ while token in src or token in existing_tokens:
+ token = create_token(n_chars)
+ counter += 1
+ if counter > 100:
+@@ -277,6 +286,7 @@ def replace_magics(src: str) -> tuple[str,
list[Replacement]]:
+ 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 = []
+@@ -292,8 +302,9 @@ def replace_magics(src: str) -> tuple[str,
list[Replacement]]:
+ 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
+@@ -313,7 +324,9 @@ def unmask_cell(src: str, replacements: list[Replacement])
-> str:
+ 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
+
+
+diff --git a/src/black/mode.py b/src/black/mode.py
+index 7335bd1..1b0965c 100644
+--- a/src/black/mode.py
++++ b/src/black/mode.py
+@@ -267,10 +267,9 @@ def get_cache_key(self) -> str:
+ + "@"
+ + ",".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),
+diff --git a/tests/test_black.py b/tests/test_black.py
+index ca19c17..a1658e7 100644
+--- a/tests/test_black.py
++++ b/tests/test_black.py
+@@ -2128,6 +2128,15 @@ def test_cache_file_length(self) -> None:
+ # 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:
+diff --git a/tests/test_ipynb.py b/tests/test_ipynb.py
+index 12afb97..aadf705 100644
+--- a/tests/test_ipynb.py
++++ b/tests/test_ipynb.py
+@@ -6,8 +6,8 @@
+ from dataclasses import replace
+
+ import pytest
+-from _pytest.monkeypatch import MonkeyPatch
+ from click.testing import CliRunner
++from pytest import MonkeyPatch
+
+ from black import (
+ Mode,
+@@ -17,7 +17,12 @@
+ 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)
+
+
[email protected]("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" ;'
+--
+2.47.3
+
diff -Nru black-25.1.0/debian/patches/series black-25.1.0/debian/patches/series
--- black-25.1.0/debian/patches/series 2025-05-27 16:35:57.000000000 +0300
+++ black-25.1.0/debian/patches/series 2026-05-06 12:45:09.000000000 +0300
@@ -3,3 +3,4 @@
docs_version.patch
privacy
click-mix-stderr.patch
+0001-Fix-some-shenanigans-with-the-cache-file-and-IPython.patch