Skip to content

Security: Path traversal in Directory.read() via crafted epub manifest (CWE-22) #359

Description

@spartan8806

Security Advisory — Path Traversal (CWE-22)

Severity: MEDIUM (CVSS 5.3 — AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)
CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory
Affected file: ebooklib/utils.py, lines 128–130
Reporter: Conner Webber (conner.webber000@gmail.com)
90-day disclosure deadline: 2026-06-06


Summary

Directory.read() in ebooklib/utils.py joins a base directory path with a subname value derived from epub OPF manifest href attributes without sanitizing for path traversal sequences:

open(os.path.join(self.directory_path, subname), "rb")

The href values from the manifest are URL-decoded via unquote (lines 1563–1607), meaning an attacker can use %2e%2e%2f (URL-encoded ../) in manifest hrefs to escape the intended directory and read arbitrary files from the filesystem.

Additionally, file_name values from the manifest are retained on EpubItem objects without sanitization, creating a zip-slip risk in any downstream code that uses these filenames for writing.

Impact

  • Arbitrary file read — A crafted epub can read files outside the epub directory when processed by ebooklib
  • Zip-slip risk — Unsanitized file_name on EpubItem objects can cause path traversal when consuming applications write these files
  • Affects any application that processes untrusted epub files with ebooklib (e-readers, converters, validators, etc.)

Steps to Reproduce

  1. Create an epub with a manifest entry containing a traversal path:
    <item id="evil" href="%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd" media-type="text/plain"/>
  2. Process the epub with ebooklib — Directory.read() will resolve and open /etc/passwd (or equivalent)

Suggested Fix

Sanitize subname in Directory.read() to reject paths containing .. components:

import os

def read(self, subname):
    # Prevent path traversal
    safe_path = os.path.normpath(subname)
    if safe_path.startswith('..') or os.path.isabs(safe_path):
        raise ValueError(f"Path traversal detected in: {subname}")
    
    full_path = os.path.join(self.directory_path, safe_path)
    # Verify resolved path is within directory_path
    if not os.path.realpath(full_path).startswith(os.path.realpath(self.directory_path)):
        raise ValueError(f"Path traversal detected in: {subname}")
    
    with open(full_path, "rb") as f:
        return f.read()

Also sanitize file_name from manifest hrefs before storing on EpubItem objects — strip directory components or reject traversal sequences.

Note

Private vulnerability reporting (PVRA) is not enabled on this repository, so this is filed as a public issue. I recommend enabling PVRA at Settings → Code security → Private vulnerability reporting for future security disclosures.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions