VAITP Dataset

← Back to the dataset

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.

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_path

Payload

/__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 ::