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

Reply via email to