The security invariant

A client-supplied name is not a path until you make it one—so make it a safe one.

01 / The boundary

The upload trusted a name it never had to store as a path.

PsiTransfer accepts files over the resumable-upload (TUS) protocol and later lets a recipient download a whole bucket as a single archive. The security-relevant fact is that the uploader chooses the file name, and that name is eventually used to build entries inside a TAR archive. A name and an archive entry path look alike, but one is untrusted input and the other is a filesystem instruction.

The upstream advisory records a path-traversal issue for versions before 2.3.1. The intake step asserted only that a name existed; it did not constrain the name to a plain basename. That left the decision about where an extracted file lands to the value an anonymous uploader supplied.

02 / Archive path

The traversal survived from upload metadata into the TAR header.

At intake, the only check on the name was that the metadata property was present. A value such as ../../../.ssh/authorized_keys satisfied that assertion. When a recipient requested the .tar.gz, the server created each TAR entry directly from the stored name, so the traversal sequence passed unchanged into the archive header.

The ZIP path behaved differently: its archive library strips leading traversal segments, which masked the problem there. The TAR writer had no equivalent guard, so it carried the full attacker-chosen path. The dangerous write does not happen on the server at upload time; it happens on the recipient’s machine when a permissive extractor writes the entry to its literal path.

Where the name becomes a path
Before the fix
  1. Upload a crafted name

    TUS metadata carries traversal segments.

  2. Assert the name exists

    Presence is checked; content is not.

  3. Write the TAR entry

    The stored name becomes the archive path.

After the fix
  1. Receive a name

    The uploaded metadata property.

  2. Force a safe basename

    Reject or reduce to a flat file name.

  3. Write the TAR entry

    The entry cannot escape the archive root.

Conceptual control flow reconstructed from the upstream source. It is not a captured request or a TraceMint execution trace.

03 / Source & fix

Constrain the name to a basename at the intake boundary.

The upstream fix adds a safe-name helper and rejects an unsafe name when the upload is created, before it can be stored. The archive-building paths then use a name derived from that safe value. Enforcement moves to the earliest point the name enters the system, so no later step has to re-derive a safe path.

The helper normalizes separators and reduces the value to a POSIX basename, which removes directory components and traversal segments. Combined with the intake rejection, the archive entry can no longer address a location outside its root.

Before · existence check onlySource
assert(meta.name, 'tus meta prop missing: name');
lib/endpoints.js0eaa8d06
Before · name becomes the TAR entrySource
const entry = pack.entry({ name: info.metadata.name, size: info.size });
lib/endpoints.js0eaa8d06
After · reject unsafe names at intakeSource
if (!utils.isSafeBasename(meta.name)) {  return res.status(400).end('Invalid file name');}
lib/endpoints.js6c71bc0b
After · reduce to a basenameSource
name = path.posix.basename(name);
lib/utils.js6c71bc0b

Selected upstream lines; indentation normalized. Links include surrounding code at immutable revisions.

04 / Impact & conditions

The write lands on the recipient, at extraction time.

The reviewed GitHub Advisory Database rates the issue High and identifies 2.3.1 as patched. Planting the archive needs no authentication, because a default deployment accepts uploads without a password. The consequential step is on the recipient side: a victim who extracts the archive with a permissive tool can have files written outside the intended directory.

An anonymous uploader
Default configuration accepts uploads without a password.
A recipient who extracts
The write occurs when the downloaded archive is unpacked.
A permissive extractor
Impact depends on the extraction tool, destination and filesystem permissions.
05 / Review & regression

List the archive before trusting it.

A regression check uploads a name containing traversal segments and then lists the produced archive without extracting it. The entry names should resolve within the archive root; an entry that escapes it is the defect. Extraction should be exercised only against a disposable destination. The scenarios below are suggested review checks, not reported TraceMint test results.

Suggested regression checks
ScenarioExpected boundary
Upload a plain file nameThe archive entry keeps the intended flat name.
Upload a traversal name into TARIntake rejects the name, or the entry resolves inside the archive root.
Upload a traversal name into ZIPThe entry cannot address a location outside the archive root.
Extract to a disposable directoryNo file is written outside the chosen destination.
06 / Lessons & limits

Sanitize the name where it enters, not where it is used.

For reviewers, treat any client-supplied name that later reaches an archive, a filesystem call or a redirect as a path until proven otherwise. The safe design forces a basename at the trust boundary, so no downstream code has to remember to do it again.

The public advisory credits the TraceMint account as reporter. The remediation is upstream work. This study analyzes the public advisory and immutable source revisions; it does not establish a private discovery sequence, a product execution method, or observed compromise of any deployment.

Source record

Follow the evidence.

Public upstream records and immutable code revisions, reviewed 2026-09-22.

  1. PsiTransfer security advisory

    Impact, prerequisites, affected range and reporter credit.

  2. Endpoints before the fix

    Immutable parent revision · 0eaa8d06 · existence-only name check and TAR entry.

  3. Upstream corrective commit

    6c71bc0b · enforce a safe basename at intake.

  4. Endpoints after the fix

    Intake rejects an unsafe name before storage.

  5. Safe-name helper

    Normalizes separators and reduces the value to a POSIX basename.

Brief code excerpts from psi-4ward/psitransfer. Upstream license: BSD 2-Clause. All remediation described here is upstream work.

Read the original record

This case study analyzes an already-public advisory and upstream source. For the complete impact, remediation and credit record, see the PsiTransfer security advisory.

Editorial publication is separate from disclosure. No private discovery timeline or method has been inferred.

More credited contributions