GHSA-c2m8-h5v5-343rHigh▾ TwilightTornado: StaticFileHandler follows symlinks outside static root (path traversal)
▾ Twilight zone — High severity, or a signal on a lesser flaw
impact 41.3 · likelihood 0 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
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.
The vulnerability is in tornado.web.StaticFileHandler, specifically in the interaction between two methods in tornado/web.py:
os.path.abspath() to resolve the requested path: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.
Complete instructions, including specific configuration details, to reproduce the vulnerability. Prerequisites: Python 3.x, Tornado installed.
mkdir -p /tmp/static
echo "DB_PASSWORD=s3cr3t" > /tmp/secret.conf
ln -s /tmp/secret.conf /tmp/static/config.conf
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()
curl http://localhost:8888/static/config.confAny application using StaticFileHandler is potentially affected if:
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).
tornado <= 6.5.8Upgrade to a patched release:
tornado 6.5.9Connected by shared product, vendor, weakness, or advisory.
GHSA-3hv7-mjh2-fv65Medium· 5.3Tornado: Unbounded query-string argument count allows event-loop-stalling DoS
GHSA-chx6-46f5-w4vpHigh· 7.5tornado: CurlAsyncHTTPClient enforces no response-size limit — decompression bomb drives unbounded memory accumulation to OOM
CVE-2024-58384Medium· 5.4Tornado before 6.4.1 contains a CRLF injection vulnerability in CurlAsyncHTTPClient that fails to reject carriage return and line feed characters in request headers
CVE-2023-54397High· 7.5Tornado before 6.3.3 contains an HTTP request smuggling vulnerability due to improper parsing of Content-Length headers accepting non-standard characters
CVE-2024-14029High· 7.5Tornado before 6.4.1 ignores duplicate Transfer-Encoding: chunked headers, treating requests as having no message body and parsing the chunked body as a subsequent request
CVE-2026-91990High· 7.5Tornado before 6.5.8 contains a memory amplification vulnerability in parse_multipart_form_data that splits multipart data before validating the max_parts limit