DEV Community

Davi
Davi

Posted on Originally published at blog.mago.team

Path Traversal in APIs: Parameter Semantics, ZIP Slip, and Encoding Bypass

Path Traversal in APIs: Parameter Semantics, ZIP Slip, and Encoding Bypass

Every path traversal tutorial starts with ?path=../../etc/passwd. That payload fails against modern APIs. The real attack surface is in /api/export?template=invoice, /api/logs?file=app.log, and /api/files/download?name=report.pdf.

These parameters announce filesystem access. The filtering behavior depends on the framework processing them and the encoding variant the tester chooses.

The parameter semantics problem

CVE-2025-68428 (CVSS 9.2) affects jsPDF in Node.js builds: addImage(), html(), addFont(), and loadFile() accept file paths supplied by the user. The process opens the file with fs.readFileSync() and embeds the content in the generated PDF. Any file readable by the Node.js process becomes system output without warning.

The parameter is not called path. It is called imageData. The file semantics live in the implementation, invisible in the API contract.

Parameters named file, filename, path, template, attachment, log, name, and export structurally imply server-side filesystem access. HackerOne #1888808 documents this precisely: download.php?filePathDownload=data_products/MISC/frida_cal/../../../../../../../../etc/passwd. The parameter name announces the semantics explicitly. The DoD server opened and returned the content without normalization.

CVE-2024-46987 (CVSS 7.7, Camaleon CMS) confirms the pattern. The MediaController passes the user-supplied path directly to send_file without prior normalization. Any authenticated user reads arbitrary files accessible to the Rails process.

Any endpoint returning Content-Disposition: attachment implies file reading from a user-controlled path. That header is a structural indicator of traversal surface, regardless of parameter name.

Encoding and filter bypass

Framework filters operate on raw input strings. Canonicalization resolves encoding after filtering. Payloads with double-encoding pass through filters that block ../ in the raw string and arrive at the filesystem decoded.

The main encoding variants are:

  • %2F encodes the slash. Filters blocking the literal / character do not block %2F.
  • %252F (double-encoding): decodes to %2F on the first pass, then to / on the second. Servers applying URL-decode twice are vulnerable.
  • %C0%AF (overlong UTF-8): encodes / in a non-standard representation accepted by some Windows decoders.
  • file%00.pdf (null byte): truncates extension checks on systems that pass the name to C-based APIs, where \0 terminates the string.
  • %5C (backslash): bypasses filters checking only the forward slash on Windows servers.

Framework behavior is specific and deterministic. Django protects static file serving by default via middleware. Custom views using open() or FileResponse receive zero middleware protection.

In Express/Node.js, path.join() alone does not prevent traversal: it normalizes the path but does not verify the base directory. path.resolve() alone does not protect either. The safe pattern requires path.resolve(basePath, userInput) followed by startsWith(basePath).

In Spring Boot, @RequestParam passed to new File() without getCanonicalPath() is fully traversable. The getCanonicalPath() call resolves symbolic links and normalizes separators. Without it, ../ in the parameter value escapes the expected directory.

Flask has no native protection for arbitrary paths. send_file() with user input requires manual validation before the call. The universal safe pattern: resolve to canonical path, then verify the result starts with the allowed base directory.

ZIP Slip: when extract() is the vector

Upload endpoints introduce a second traversal class where the vector is the archive itself, not a path parameter. When ZIP archive members contain ../ in their names, extraction writes outside the target directory. The library decides whether to validate those names before creating the file on disk. The developer normally assumes it does.

Snyk documented ZIP Slip in 2018 with research covering Java, JavaScript, .NET, Go, and Ruby. The Java libraries plexus-archiver, zip4j, and adm-zip extract without validating each member's destination. In Node.js: adm-zip is vulnerable by default; unzipper was vulnerable before version 0.8.13. In Python: zipfile.extractall() is safe by default; the tarfile module is still affected in direct use. In Go: mholt/archiver was vulnerable before the patch.

CVE-2024-13059 in AnythingLLM demonstrates production impact. The upload handler uses multer. Filenames with non-ASCII characters bypass path validation in the upload handler. Users with the manager or admin role write files to arbitrary server locations. This leads to remote code execution. The fix arrived in version 1.3.1.

node-tar generated two CVEs in 2021: CVE-2021-32803 and CVE-2021-32804. Both involve archive members with names containing ../ that escape the destination directory during extraction. The impact on dependent projects was broad given the library's weekly download volume.

Safe extraction pattern: for each archive member, resolve the destination path. Verify it starts with the base directory before creating any file on disk. Validating after extraction protects nothing, because the malicious file has already been written.

CVEs and HackerOne: traversal in production endpoints

Path traversal with CVSS 8.0+ appears consistently in request body parameters, not in URL path segments. WAFs apply pattern matching on URLs. JSON body parameters reach the application handler without passing through network inspection layers.

CVE-2026-42048 (CVSS 9.6) affects Langflow. The DELETE /api/v1/knowledge_bases endpoint accepts a name parameter concatenated directly into the filesystem path. The attacker deletes arbitrary directories. CVE-2026-42867 affects POST /api/v1/knowledge_bases in the same software: the name parameter creates files and directories anywhere on the filesystem. Both affect versions before 1.9.0.

CVE-2025-58438 (CVSS 9.4) in internetarchive: the file.download() method does not sanitize user-supplied filenames. A malicious name writes content outside the target directory. The vulnerability class is identical to the Content-Disposition pattern.

HackerOne #733072 (GitLab Package Registry) is the highest-impact documented case in this class. Traversal in the registry API allows writing arbitrary files to any location accessible to the git user. The chain ends in RCE via overwriting Git configuration files.

HackerOne #1888808 (DoD) and CVE-2025-68428 (jsPDF, CVSS 9.2) document the base pattern: a filename parameter goes directly to a filesystem read operation. The difference is the delivery context: direct download versus silent embedding in a generated PDF.

Detection methodology

Systematic probing starts with identifying the parameter that implies filesystem access. The payload and encoding variant are determined afterward, based on the identified framework.

First step: scan API traffic and documentation for file, filename, path, template, attachment, log, name, export, report, download. Response classification confirms the surface: file bytes, rendered template, log lines, or compressed content extraction confirm filesystem access.

For blind traversal: OOB DNS callback with the payload in the subdomain identifies filesystem access without content returned in the response. The base wordlist uses /etc/passwd for Linux and \windows\win.ini for Windows. For extraction endpoints: build a ZIP with a member named ../../../evil and observe the extraction destination.

The encoding sequence follows the identified framework: raw ../, %2F, %252F (double-encoding), %C0%AF, null byte, %5C. ffuf with a path wordlist and encoding variations automates horizontal coverage across multiple endpoints simultaneously.

Mapping: parameter type to vector

Parameter type Traversal class Reference CVE
Filename in upload Arbitrary write / ZIP Slip CVE-2024-13059 (AnythingLLM)
Filename in file download Arbitrary read CVE-2025-68428 (jsPDF), H1 #1888808
Compressed file extraction ZIP Slip CVE-2021-32803 (node-tar)
Report or template export Local file inclusion CVE-2026-42048 (Langflow)
Template rendering Direct path traversal CVE-2024-46987 (Camaleon CMS)

The entry point of any test is the parameter type. The payload is determined by the framework detected in the target application's stack.

(MAGO team tool) automates the first step. The tech_detector capability identifies file-serving endpoints, extraction endpoints, and template rendering surfaces from URL patterns and Content-Disposition headers in responses.

The filter question is framework-specific. Django stops raw ../ in request routing, but not in custom views using open(). Express stops nothing without path.resolve() plus prefix verification. Java stops nothing without File.getCanonicalPath() followed by startsWith(). The encoding variant to test first is determined by the detected stack, not by frequency of appearance in public writeups.

Top comments (0)