GNU tar prefixes every entry with ./ when archiving a directory
(tar czf out.tgz . yields ./index.html, ./bin_bg.wasm, …). The path
validator rejected any '.' segment as traversal, so such tarballs failed
with "archive entry has an unsafe path: './...'" and their ./index.html
never satisfied the root-index.html requirement — making ./-prefixed
archives unusable. (.wasm was never the issue: extensions aren't checked
and mime.rs maps .wasm to application/wasm.)
check_entry_path now strips '.' segments (leading and interior) and
returns the normalized path used as the R2 key, while still rejecting
'..', absolute paths, backslashes, NUL, drive letters, empty (//)
segments, and names that normalize to nothing; depth is measured on the
normalized path. Both the zip and tar manifest builders store the
normalized name. +10 tests.
Bean: pages-aalu.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
title: Normalize '.' path segments so ./-prefixed tar entries are accepted
status: completed
type: bug
priority: high
created_at: 2026-07-21T16:25:26Z
updated_at: 2026-07-21T16:30:49Z
---
## Symptom
Uploading a GNU-tar-style tarball fails with `archive entry has an unsafe path: './bin_bg.wasm'` (a `PathTraversal` 400). Not wasm-specific — `.wasm` is fully allowed (mime.rs maps it to application/wasm; extensions are not checked on upload).
## Root cause
`check_entry_path` (core/upload.rs ~664-670) splits the entry name on `/` and rejects any segment equal to `.` (alongside `..` and empty). GNU `tar czf out.tgz .` prefixes every entry with `./` (e.g. `./index.html`, `./bin_bg.wasm`), so the leading `.` segment trips the traversal guard. This also breaks the root-`index.html` requirement for such tarballs (`./index.html` != `index.html`), making `./`-prefixed archives unusable. Zip tools don't emit `./`, so only the new compressed-tar path exposed this.
## Fix
Treat `.` segments as the no-ops they are: normalize them out (strip leading `./`, drop interior `.` segments) and use the normalized `/`-joined path as the stored R2 key. Keep rejecting `..`, absolute paths, backslashes, NUL, drive letters, over-length/over-depth, and empty-after-normalization. Apply to both zip and tar paths for consistency (share via `check_entry_path`). Depth is measured on the normalized path.
## Tasks
- [x] Normalize `.` segments in check_entry_path; return the normalized path for use as the R2 key
- [x] Update zip + tar manifest builders to store the normalized path (so index.html detection sees the normalized name)
- [x] Tests: `./`-prefixed tar entry accepted (incl. ./index.html satisfies root requirement and ./bin_bg.wasm accepted), interior `./` normalized, `..` still rejected, `.`-only / empty-after-normalization rejected
- [x] All six validation commands pass (native + wasm)
- [x] Commit, push, deploy
## Summary of Changes
`check_entry_path` now returns the normalized path (`Result<String,_>`): `.` current-directory segments are stripped (leading `./` and interior `/./`) instead of rejected, while `..`, absolute paths, backslashes, NUL, drive letters, empty (`//`) segments, and names that normalize to nothing are still rejected. Depth is measured on the normalized segment count. Both `build_zip_manifest` and `read_tar_entries` store the normalized path, so a GNU-tar `./index.html` satisfies the root-level requirement and `./bin_bg.wasm` is accepted.
`.wasm` was never blocked — the upload validator does not check extensions, and `mime.rs` maps `.wasm` to `application/wasm`.
+10 tests: 3 tar `./`-prefix wrappers (gzip/zstd/xz) plus 7 direct `check_entry_path` cases (leading/interior `.` normalization, `..` still rejected, `.`-only rejected, empty segment rejected, depth measured post-normalization). All six validation commands pass: 196 unit + 20 doctests.