Skip to content

_joinDirectoryName traversal check bypassable via directory prefix collision #1061

Description

@vnykmshr

The path traversal check in _joinDirectoryName() compares string prefixes, not filesystem paths. It can be bypassed when uploadDir doesn't end with a path separator (the default).

// Current check (src/Formidable.js, line 598):
const newPath = path.join(this.uploadDir, name);
if (!newPath.startsWith(this.uploadDir)) { ... }

When uploadDir = "/tmp/uploads":

path.join("/tmp/uploads", "../uploads-evil/x")  →  "/tmp/uploads-evil/x"
"/tmp/uploads-evil/x".startsWith("/tmp/uploads")  →  true  ← bypassed

The resolved path escapes the intended directory but passes the check because it shares a string prefix. os.tmpdir() (the default uploadDir) never returns a trailing separator on any platform, so the check is broken for default configurations.

This affects applications using the filename callback that return values derived from part.originalFilename. Combined with createDirsFromUploads: true, the sibling directory gets created automatically.

Introduced in #689 (v2.0.0), present in all v2.x and v3.x releases through v3.5.4 (current).

Fix: append path.sep to the resolved directory before comparison:

_joinDirectoryName(name) {
    const resolvedDir = path.resolve(this.uploadDir);
    const resolvedPath = path.resolve(resolvedDir, name);

    if (resolvedPath === resolvedDir || !resolvedPath.startsWith(resolvedDir + path.sep)) {
      return path.join(this.uploadDir, this.options.defaultInvalidName);
    }

    return resolvedPath;
}

I'll open a PR with this fix and tests.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions