A client-supplied name is not a path until you make it one—so make it a safe one.
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.
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.
- Upload a crafted name
TUS metadata carries traversal segments.
- Assert the name exists
Presence is checked; content is not.
- Write the TAR entry
The stored name becomes the archive path.
- Receive a name
The uploaded metadata property.
- Force a safe basename
Reject or reduce to a flat file name.
- 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.
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.
assert(meta.name, 'tus meta prop missing: name');const entry = pack.entry({ name: info.metadata.name, size: info.size });if (!utils.isSafeBasename(meta.name)) { return res.status(400).end('Invalid file name');}name = path.posix.basename(name);Selected upstream lines; indentation normalized. Links include surrounding code at immutable revisions.
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.
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.
| Scenario | Expected boundary |
|---|---|
| Upload a plain file name | The archive entry keeps the intended flat name. |
| Upload a traversal name into TAR | Intake rejects the name, or the entry resolves inside the archive root. |
| Upload a traversal name into ZIP | The entry cannot address a location outside the archive root. |
| Extract to a disposable directory | No file is written outside the chosen destination. |
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.
Follow the evidence.
Public upstream records and immutable code revisions, reviewed 2026-09-22.
- PsiTransfer security advisory
Impact, prerequisites, affected range and reporter credit.
- Endpoints before the fix
Immutable parent revision · 0eaa8d06 · existence-only name check and TAR entry.
- Upstream corrective commit
6c71bc0b · enforce a safe basename at intake.
- Endpoints after the fix
Intake rejects an unsafe name before storage.
- 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.