CVE-2026-39847
Path traversal in Emmett's static handler allows arbitrary file reading.
- CVSS 7.5
- CWE-22
- Input Validation and Sanitization
- Remote
Emmett is a full-stack Python web framework designed with simplicity. From 2.5.0 to before 2.8.1, the RSGI static handler for Emmett's internal assets (/__emmett__ paths) is vulnerable to path traversal attacks. An attacker can use ../ sequences (eg /__emmett__/../rsgi/handlers.py) to read arbitrary files outside the assets directory. This vulnerability is fixed in 2.8.1.
- CWE
- CWE-22
- CVSS base score
- 7.5
- Published
- 2026-04-07
- OWASP
- A01 Broken Access Control
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Path Traversal
- Accessibility scope
- Remote
- Impact
- Information Disclosure
- Affected component
- Emmett
- Fixed by upgrading
- Yes
Solution
Upgrade Emmett to version 2.8.1 or later.
Vulnerable code sample
import os
import sys
# This simulates the logic for defining the root path for internal assets.
if getattr(sys, 'frozen', False):
EMMETT_ASSETS_PATH = os.path.join(sys._MEIPASS, 'assets')
else:
EMMETT_ASSETS_PATH = os.path.join(os.path.dirname(__file__), 'assets')
async def vulnerable_rsgi_static_handler(scope, receive, send):
"""
This function is a representation of the vulnerable RSGI static file handler
in Emmett versions prior to 2.8.1. It is intended for demonstration purposes only.
"""
if scope['type'] != 'http':
return
path = scope['path']
if not path.startswith('/__emmett__/'):
return
relative_path = path[len('/__emmett__/'):]
# The vulnerability lies here: user input is joined to a path without sanitization.
# An attacker can use '..' to traverse the filesystem.
file_path = os.path.join(EMMETT_ASSETS_PATH, relative_path)
# To avoid 'file_path' being an absolute path, which os.path.join allows
# if the second argument starts with '/', we naively strip it.
# This was part of the original logic that still allowed traversal.
if os.path.isabs(file_path):
file_path = file_path.lstrip('/')
try:
with open(file_path, "rb") as file:
content = file.read()
await send({
'type': 'http.response.start',
'status': 200,
'headers': [
(b'content-type', b'application/octet-stream')
]
})
await send({'type': 'http.response.body', 'body': content})
except (FileNotFoundError, IsADirectoryError):
await send({
'type': 'http.response.start',
'status': 404,
'headers': [(b'content-type', b'text/plain')]
})
await send({'type': 'http.response.body', 'body': b'Not Found'})Patched code sample
import os
def get_secure_path(base_dir, requested_path):
"""
This function represents the fix for a path traversal vulnerability.
It takes a base directory and a user-requested path and returns a safe,
absolute file path if it exists within the base directory, otherwise None.
"""
# 1. Sanitize the requested path by removing any leading slashes
# to prevent it from being treated as an absolute path.
# os.path.join handles this, but it's good practice.
safe_request_path = requested_path.lstrip('/\\')
# 2. Join the trusted base directory with the sanitized requested path.
file_path = os.path.join(base_dir, safe_request_path)
# 3. Resolve the path to its absolute, canonical form. This is the
# crucial step that resolves any ".." sequences (e.g., /a/b/../c -> /a/c).
# os.path.realpath also follows symlinks, adding another layer of security.
real_path = os.path.realpath(file_path)
# 4. Get the absolute path of the trusted base directory.
real_base_dir = os.path.realpath(base_dir)
# 5. The security check: verify that the resolved path of the requested
# file is still inside the trusted base directory.
# os.path.commonpath is used for a robust, cross-platform check.
if os.path.commonpath([real_path, real_base_dir]) != real_base_dir:
# The path has "escaped" the intended directory.
# A real application would log this and return a 404 or 403 error.
return None
# 6. Finally, ensure the path points to an actual file.
if not os.path.isfile(real_path):
return None
return real_pathPayload
/__emmett__/../../../../../../etc/passwd
Cite this entry
@misc{vaitp:cve202639847,
title = {{Path traversal in Emmett's static handler allows arbitrary file reading.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-39847},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-39847/}}
}
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 ::
