Skip to content

Reject unsafe index names in the index server (path traversal) - #337

Merged
folbricht merged 1 commit into
masterfrom
fix/index-server-path-traversal
May 17, 2026
Merged

folbricht merged 1 commit into
masterfrom
fix/index-server-path-traversal

Conversation

@folbricht

Copy link
Copy Markdown
Owner

Problem

HTTPIndexHandler.ServeHTTP derived the index name via path.Base(r.URL.Path) (httpindexhandler.go), which treats only / as a separator. LocalIndexStore.GetIndexReader/StoreIndex (localindex.go) then concatenated that name onto the store path (os.Open/os.Create(s.Path + name)) with no filtering.

On Windows \ is a path separator, so a URL-decoded path like /..\..\..\target (e.g. PUT /..%5C..%5C..%5CUsers%5Cvictim%5C.ssh%5Cauthorized_keys) resolved outside the store directory:

  • PUT truncates/creates arbitrary files on the host before writing the index body.
  • GET/HEAD act as a file-existence oracle (404-vs-400) for arbitrary paths even on a read-only server.

Server auth is optional and off by default. http.ServeMux's cleanPath uses forward-slash path.Clean and leaves backslashes intact, so the payload reaches the handler unsanitized. The chunk server is not affected — idFromPath strictly validates the /<prefix>/<id>.ext shape and runs ChunkIDFromString.

Fix

  • Add validIndexName(): rejects empty / . / .., any name containing / or \ (both hardcoded, so a cross-platform payload is rejected regardless of host OS), and anything filepath.IsLocal flags (Windows reserved device names, volume/absolute paths).
  • Enforce it in HTTPIndexHandler.ServeHTTP immediately after deriving the name — a flat 400 for GET/HEAD/PUT, which also closes the existence oracle.
  • Enforce it in LocalIndexStore.GetIndexReader/StoreIndex as defense in depth so non-HTTP callers are protected too (GetIndex is covered transitively).

Tests

New TestIndexServerPathTraversal starts a writable server with a nested store dir and asserts the ..%5C..%5Cevil.caibx payload yields 400 for PUT/GET/HEAD and that no file is created above the store root. Because the rejection is OS-independent, the test passes on the existing Linux/Windows/macOS CI.

go build ./..., go vet ./..., gofmt -l, go test . and go test ./cmd/desync -run TestIndexServer all pass; the pre-existing read/write index-server tests are unaffected.

HTTPIndexHandler derived the index name via path.Base(r.URL.Path),
which only treats '/' as a separator, and LocalIndexStore concatenated
that name onto the store path with no filtering. On Windows '\' is a
path separator, so a URL-decoded path like '/..\..\..\target' escaped
the store directory: PUT could truncate/create arbitrary files and
GET/HEAD acted as a file-existence oracle. Auth is off by default.

Add validIndexName() rejecting empty/'.'/'..', any name containing '/'
or '\' (both hardcoded so cross-platform payloads are caught on any
host), and anything filepath.IsLocal flags. Enforce it in
HTTPIndexHandler.ServeHTTP (flat 400 for GET/HEAD/PUT, closing the
oracle) and in LocalIndexStore.GetIndexReader/StoreIndex as defense in
depth for non-HTTP callers.
@folbricht
folbricht merged commit c0cb346 into master May 17, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

⌂ 换网址