Skip to content
Afa' Afa'

Fixing a Path Traversal Vulnerability in image-downloader

How CVE-2026-103648, a critical path traversal (CWE-22) in image-downloader, was reported, fixed in 4.3.1 and covered by a regression test.

6 min read

Summarise this page withyour favorite AI assistant

On October 2, 2026, I released image-downloader 4.3.1, a security release fixing a path traversal vulnerability affecting previous versions of the package.

The vulnerability has been assigned CVE-2026-103648 and is classified as CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal"). It has a CVSS v3.1 score of 9.1 (Critical).

This post explains what happened, why the vulnerability existed, how it was fixed, and what I learned from handling the disclosure as an open source maintainer.

About image-downloader

image-downloader is a small Node.js module whose purpose is straightforward: download an image from a URL and save it to disk.

A typical usage looks like this:

js
const download = require('image-downloader');

const options = {
  url: 'https://example.com/image.jpg',
  dest: '/path/to/dest',
};

download.image(options)
  .then(({ filename }) => {
    console.log('Saved to', filename);
  })
  .catch(console.error);

When dest is a directory, the library automatically extracts the filename from the URL.

That automatic filename extraction was at the heart of the vulnerability.

The vulnerability

The vulnerable versions are prior to 4.3.1, including 4.3.0.

The problem was related to the order in which the URL pathname was processed. The filename was extracted using path.basename() before URL decoding took place. That sounds like a small implementation detail, but it created an important difference between these two operations:

text
URL pathname
    ↓
basename()
    ↓
decode

and the safer order:

text
URL pathname
    ↓
decode
    ↓
basename()

An encoded path separator such as %2f is not initially interpreted as /.

Consequently, applying path.basename() before decoding could leave an encoded separator inside the resulting filename. Once the value was subsequently decoded, that separator could become an actual path separator.

The resulting path could therefore escape the directory supplied through dest.

In other words, data that was supposed to be written below:

text
/uploads/

could potentially be resolved somewhere outside that directory.

Why this matters

This is a server-side vulnerability.

An application is exposed when it combines the vulnerable library with an attacker-controlled download URL and a writable destination directory.

Under those conditions, an attacker could cause downloaded data to be written outside the intended directory.

The exact impact depends on the privileges of the process running the application and on which files are writable.

This is why path traversal vulnerabilities deserve particular attention in libraries that write files to disk: the vulnerable component may be small, but its security boundary is defined by the application using it.

The CVE record currently describes the vulnerability as allowing an attacker who controls the download URL to cause downloaded response data to be written outside the configured destination directory.

How it was fixed

The fix in 4.3.1 does not simply disable automatic filename extraction.

I chose to preserve the existing behavior because automatic extraction is part of the normal API and changing that default would have been a breaking change.

Instead, the filename handling was made defensive.

The new logic:

  1. Decodes the URL pathname before extracting the filename.
  2. Extracts the basename from the decoded pathname.
  3. Normalizes the resulting path.
  4. Resolves the final destination.
  5. Verifies that the resolved path remains inside the configured destination directory.
  6. Rejects paths that escape the destination.
  7. Rejects malformed percent-encoding.
  8. Rejects NUL bytes.

The important security property is therefore not just "sanitize the filename", but verify the final resolved path against the intended directory.

This provides a much stronger boundary than relying on a particular representation of the input path.

Regression tests

A security fix is not complete without a regression test.

The test suite was extended to cover the traversal scenario that triggered the disclosure, as well as the relevant path-handling edge cases.

The goal was not only to make the original proof of concept fail, but also to make the security property explicit:

A URL-derived filename must never allow the downloaded file to escape the configured destination directory.

That property should remain true regardless of how the path is encoded in the URL.

Coordinated disclosure

The vulnerability was privately reported by Amirhossein Roustaei (@eternullsec) of EterNull Security.

The report was reproduced, the root cause was identified, and a fix was prepared with regression tests.

The reporter then reviewed the proposed remediation and the security advisory before publication.

Once the fix was released as 4.3.1, the CVE was published as CVE-2026-103648. The public CVE record identifies versions before 4.3.1 as affected and 4.3.1 as the fixed release.

I would like to thank Amirhossein for the responsible disclosure, the detailed report, and the constructive collaboration throughout the process.

The release

The fix is available in:

text
image-downloader >= 4.3.1

If you are using an affected version, upgrading is the recommended remediation.

bash
npm install [email protected]

The vulnerability is tracked as:

CVE-2026-103648

CWE-22 — Path Traversal

CVSS v3.1: 9.1 (Critical)

text
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H

The CVE record is publicly available through the CVE ecosystem, and the vulnerability has also been indexed by the NVD.

What I learned

The most interesting part of this incident for me was not the amount of code involved.

The vulnerable behavior came from a very small difference in the order of two perfectly reasonable operations:

text
basename()
decode()

versus:

text
decode()
basename()

When dealing with untrusted paths, representation and normalization are security concerns.

A string that looks like a filename is not necessarily a filename.

Before trusting it, you need to know what it becomes after decoding, normalization, and resolution.

The other important lesson is that a path containment check is often a better security boundary than attempting to enumerate every possible traversal representation.

Instead of asking:

"Did I remove every dangerous sequence?"

the safer question is:

"Where does this path actually resolve, and is that location inside the directory where I intended to write?"

That distinction is small in code, but significant from a security perspective.

Final thoughts

image-downloader is a relatively small open source project, but this incident was a good reminder that even small filesystem utilities can become part of an application's security boundary.

I am grateful to the reporter for bringing the issue forward responsibly and for reviewing the remediation before publication.

The vulnerability is now fixed in 4.3.1, and CVE-2026-103648 provides a permanent public record of the issue and its remediation.

For more information about the project, see the image-downloader project page.

References

Something wrong or want to discuss this article? Get in touch

Search articles and projects

Type to filter articles and projects. Use the arrow keys to move through results and Enter to open one. Press Escape to close.