GHSA-c2m8-h5v5-343r
Tornado: StaticFileHandler follows symlinks outside static root (path traversal)
At a glance
- Published
- Updated
- CVE
- CVE-2026-103263
- CVSS_V4
- CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
Description
### Summary StaticFileHandler allows an unauthenticated attacker to read arbitrary files from the server's filesystem by requesting a path that resolves to a symbolic link placed inside the static root directory. Any application that serves user-uploadable content, or whose static directory is populated by a build/deploy pipeline that creates symlinks (e.g. npm link, webpack, Docker volume mounts, CDN sync tools), is affected. An attacker who can trigger the creation of a symlink pointing outside the static root—or exploit one that already exists—can retrieve sensitive files such as /etc/passwd, private keys, configuration files, or application secrets. ### Details The vulnerability is in tornado.web.StaticFileHandler, specifically in the interaction between two methods in [tornado/web.py](vscode-webview://1grdaaa6v64itrol06qu70pq3d1b9h9im5c3npvvc9snaprc1q9f/tornado/web.py): - [get_absolute_path](https://github.com/tornadoweb/tornado/blob/0ca3c5f8279d402b245718d16522bc18a8f5d958/tornado/web.py#L2932) uses `os.path.abspath()` to resolve the requested path: - [validate_absolute_path](https://github.com/tornadoweb/tornado/blob/0ca3c5f8279d402b245718d16522bc18a8f5d958/tornado/web.py#L2935) uses the same `os.path.abspath()` on the root, then performs a string prefix check: `os.path.abspath()` normalises . and .. segments but does not resolve symbolic links. As a result, a path like `/var/www/static/link` passes the `startswith("/var/www/static/")` check regardless of where `link` actually points. Immediately after, `os.path.exists()` and `os.path.isfile()` **do follow symlinks**, so the file they ultimately open is the symlink's target. The fix would be to replace os.path.abspath() with os.path.realpath() in both methods, so the resolved real path of the symlink target is validated against the root, not just its string representation inside the static directory. ### PoC _Complete instructions, including specific configuration details, to reproduce the vulnerability._ Prerequisites: Python 3.x, Tornado installed. 1. Create the environment ``` mkdir -p /tmp/static echo "DB_PASSWORD=s3cr3t" > /tmp/secret.conf ln -s /tmp/secret.conf /tmp/static/config.conf ``` 2. Minimal vulnerable server (server.py): ``` import tornado.web, tornado.ioloop app = tornado.web.Application([ (r"/static/(.*)", tornado.web.StaticFileHandler, {"path": "/tmp/static"}), ]) app.listen(8888) tornado.ioloop.IOLoop.current().start() ``` 3. Exploit: `curl http://localhost:8888/static/config.conf` ### Impact Any application using StaticFileHandler is potentially affected if: - the static directory contains symlinks pointing outside it (common with build tooling), or - the application allows file uploads into the static directory without stripping symlinks. An unauthenticated remote attacker can read any file readable by the process user: application secrets, private TLS keys, database credentials, /etc/shadow, SSH keys, or source code (depending on the process's filesystem permissions).
Affected packages
| Package | Versions |
|---|---|
| PyPI · tornado | introduced 0, fixed 6.5.9 |
References
- https://github.com/tornadoweb/tornado/security/advisories/GHSA-c2m8-h5v5-343r WEB
- https://github.com/tornadoweb/tornado/pull/3719 WEB
- https://github.com/tornadoweb/tornado/commit/437ab5f76565403b3403438d2e555d11f8128d32 WEB
- https://github.com/tornadoweb/tornado/commit/f9f50b8bab265a2abf9f2415a3d2da6264a5450b WEB
- https://github.com/tornadoweb/tornado PACKAGE
- https://github.com/tornadoweb/tornado/releases/tag/v6.5.9 WEB
Sources
| Source | Licence |
|---|---|
| OSV.dev | CC-BY 4.0 — GitHub Advisory Database |