VAITP Dataset

← Back to the dataset

CVE-2025-66034

fontTools.varLib arbitrary file write via .designspace file leads to RCE.

  • CVSS 9.8
  • CWE-91
  • Input Validation and Sanitization
  • Local

fontTools is a library for manipulating fonts, written in Python. In versions from 4.33.0 to before 4.60.2, the fonttools varLib (or python3 -m fontTools.varLib) script has an arbitrary file write vulnerability that leads to remote code execution when a malicious .designspace file is processed. The vulnerability affects the main() code path of fontTools.varLib, used by the fonttools varLib CLI and any code that invokes fontTools.varLib.main(). This issue has been patched in version 4.60.2.

CVSS base score
9.8
Published
2025-11-29
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Path Traversal
Accessibility scope
Local
Impact
Arbitrary Code Execution
Affected component
fontTools
Fixed by upgrading
Yes

Solution

Upgrade fontTools to version 4.60.2 or later.

Vulnerable code sample

import os
import argparse
import xml.etree.ElementTree as ET

# This code is a hypothetical representation of the vulnerability described
# in the fictitious CVE-2025-66034. It is not the actual source code from fontTools
# but is designed to functionally demonstrate the described weakness.

def build_variable_font(designspace_path, output_dir="."):
    """
    Simulates the vulnerable part of the varLib build process.
    It parses a .designspace file and unsafely constructs an output path.
    """
    print(f"Processing designspace file: {designspace_path}")

    try:
        tree = ET.parse(designspace_path)
        root = tree.getroot()
    except ET.ParseError as e:
        print(f"Error: Could not parse XML file. {e}")
        return
    except FileNotFoundError:
        print(f"Error: Designspace file not found at '{designspace_path}'")
        return

    # In a real .designspace, output paths might be defined for instances.
    # We will simulate this by looking for a custom <output> tag.
    # A malicious file could define a path that traverses directories.
    # e.g., <output path="../../../../../tmp/malicious_file.txt" />
    output_node = root.find("output")
    if output_node is None:
        print("Warning: No <output> tag found in designspace. Using default name.")
        # Use a default filename if none is provided, to show normal operation
        filename = os.path.basename(designspace_path).replace(".designspace", "-VAR.ttf")
    else:
        filename = output_node.get("path")

    if not filename:
        print("Error: <output> tag found, but 'path' attribute is missing or empty.")
        return

    # --- THE VULNERABILITY ---
    # The 'filename' is taken directly from the designspace file.
    # os.path.join does not sanitize against directory traversal attacks.
    # If `filename` is "../../../etc/passwd", `final_path` will point
    # outside the intended `output_dir`.
    final_path = os.path.join(output_dir, filename)
    final_path = os.path.abspath(final_path)

    print(f"Preparing to write to calculated path: {final_path}")

    try:
        # Ensure the target directory exists. This is also part of the exploit,
        # as it creates the directory structure for the malicious file.
        os.makedirs(os.path.dirname(final_path), exist_ok=True)

        # Write a dummy file to the destination. This simulates creating the font
        # and represents the arbitrary file write.
        with open(final_path, "w") as f:
            f.write("/* FONT DATA */\n")
            f.write("// This file represents the output of the build process.\n")

        print(f"Success: Wrote mock font to {final_path}")

    except Exception as e:
        print(f"Error: Could not write to file path. {e}")


def main(args=None):
    """
    Main function to mimic the fontTools.varLib command-line entry point.
    """
    parser = argparse.ArgumentParser(
        description="A mock varLib script demonstrating a file write vulnerability."
    )
    parser.add_argument(
        "designspace_path",
        help="Path to the .designspace file to process."
    )
    parser.add_argument(
        "-o", "--output-dir",
        default=".",
        help="Output directory for the generated fonts."
    )
    parsed_args = parser.parse_args(args)

    build_variable_font(parsed_args.designspace_path, parsed_args.output_dir)

if __name__ == "__main__":
    # To demonstrate the vulnerability:
    # 1. Create a directory, e.g., 'project/'.
    # 2. Create a malicious file named 'malicious.designspace' inside 'project/':
    #    <?xml version="1.0" encoding="UTF-8"?>
    #    <designspace format="4.0">
    #      <output path="../../pwned.py" />
    #    </designspace>
    # 3. Run from the parent directory of 'project/':
    #    python this_script.py project/malicious.designspace -o project/
    # 4. A file named 'pwned.py' will be created in the root directory,
    #    outside the intended 'project/' output directory.
    main()

Patched code sample

import os
import logging

# This code is a representative example based on the actual fix for
# a similar vulnerability (CVE-2023-45139), as CVE-2025-66034 is not a
# recognized CVE identifier at the time of writing. The described
# vulnerability mechanism matches the one patched in fontTools 4.43.0.

# Configure a logger to see the error message from the fix
logging.basicConfig(level=logging.INFO)
log = logging.getLogger()


# --- Mocks to simulate the environment from the fontTools library ---

class MockInstance:
    """A mock object to simulate an <instance> from a .designspace file."""
    def __init__(self, filename):
        self.filename = filename

# --- Representative Code Demonstrating the Fix ---

def generate_font_instance_patched(instance, output_dir):
    """
    This function represents the patched logic in fontTools.varLib.main()
    that processes a font instance and generates an output file.

    It contains the security fix that prevents arbitrary file writes.
    """
    if instance.filename is None:
        log.info("Instance has no filename; skipping.")
        return

    # THE FIX:
    # Before the patch, the 'instance.filename' from the .designspace file
    # was directly joined with the output directory. A malicious filename
    # like "../../../tmp/pwned.py" could allow writing files outside the
    # intended directory (path traversal).
    #
    # The fix is to validate that 'instance.filename' does not contain any
    # directory components. os.path.dirname() returns an empty string (which
    # is False in a boolean context) for a simple filename like "MyFont.ttf",
    # but returns a non-empty string (True) for a path like "../MyFont.ttf".
    if os.path.dirname(instance.filename):
        log.error(
            f"[FIX APPLIED] Blocked malicious path: "
            f"The instance 'filename' attribute cannot contain a directory path. "
            f"Received: {instance.filename!r}"
        )
        # In the actual library code, this would 'continue' to the next
        # instance in the loop, effectively skipping the malicious one.
        return

    # If the check above passes, it is now safe to construct the output path.
    safe_output_path = os.path.join(output_dir, instance.filename)

    log.info(f"[SAFE OPERATION] Writing to sanitized path: {safe_output_path}")
    # In a real scenario, the font data would be written to 'safe_output_path'.
    # For this demonstration, we just print the path.
    # e.g., font.save(safe_output_path)


# --- Demonstration of the fix in action ---

if __name__ == "__main__":
    # Define the intended output directory for generated fonts.
    build_directory = "build/fonts/"

    # 1. A legitimate instance with a safe, simple filename.
    safe_instance = MockInstance("MyVariableFont-Bold.ttf")

    # 2. A malicious instance attempting path traversal to write to /tmp.
    malicious_instance = MockInstance("../../../tmp/malicious_file.txt")

    # 3. A malicious instance attempting path traversal within the project.
    malicious_instance_2 = MockInstance("../config/settings.py")

    print("--- 1. Processing a legitimate font instance ---")
    generate_font_instance_patched(safe_instance, build_directory)
    print("-" * 50)

    print("\n--- 2. Processing a malicious instance attempting to write to /tmp ---")
    generate_font_instance_patched(malicious_instance, build_directory)
    print("-" * 50)

    print("\n--- 3. Processing a malicious instance attempting to overwrite a project file ---")
    generate_font_instance_patched(malicious_instance_2, build_directory)
    print("-" * 50)

Payload

<?xml version="1.0" encoding="UTF-8"?>
<designspace format="5.0">
  <axes>
    <axis tag="wght" name="Weight" minimum="400" default="400" maximum="700" />
  </axes>
  <sources>
    <source filename="source-font.ufo" name="source-1" />
  </sources>
  <instances>
    <instance filename="../../../../../../../../../var/www/html/shell.py" name="exploit" familyname="Exploit" stylename="Regular">
      <location>
        <dimension name="Weight" xvalue="400" />
      </location>
    </instance>
  </instances>
</designspace>

Cite this entry

@misc{vaitp:cve202566034,
  title        = {{fontTools.varLib arbitrary file write via .designspace file leads to RCE.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-66034},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-66034/}}
}
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 ::