netbirdio/netbird · warning
invalid path
Error message
invalid path
What it means
HTTP 400 returned by the local-storage upload handler (PUT /upload/{dir}/{file}) when the {dir} path segment, after filepath.Join with the base directory and filepath.Clean, no longer has the cleaned base (STORE_DIR, default /var/lib/netbird, plus a trailing separator) as a prefix. It is a deliberate path-traversal guard: the server logs 'Path traversal attempt blocked (dir)' and refuses the request before creating anything.
Source
Thrown at upload-server/server/local.go:116
return
}
uploadDir := r.PathValue("dir")
if uploadDir == "" {
http.Error(w, "missing dir path", http.StatusBadRequest)
return
}
uploadFile := r.PathValue("file")
if uploadFile == "" {
http.Error(w, "missing file name", http.StatusBadRequest)
return
}
cleanBase := filepath.Clean(l.dir) + string(filepath.Separator)
dirPath := filepath.Clean(filepath.Join(l.dir, uploadDir))
if !strings.HasPrefix(dirPath, cleanBase) {
http.Error(w, "invalid path", http.StatusBadRequest)
log.Warnf("Path traversal attempt blocked (dir): %s", dirPath)
return
}
filePath := filepath.Clean(filepath.Join(dirPath, uploadFile))
if !strings.HasPrefix(filePath, cleanBase) {
http.Error(w, "invalid path", http.StatusBadRequest)
log.Warnf("Path traversal attempt blocked (file): %s", filePath)
return
}
if err = os.MkdirAll(dirPath, 0750); err != nil {
http.Error(w, "failed to create upload dir", http.StatusInternalServerError)
log.Errorf("Failed to create upload dir: %v", err)
return
}
flags := os.O_WRONLY | os.O_CREATE | os.O_EXCLView on GitHub (pinned to 93e97f4bf1)
Solutions
- Use the PUT URL returned by GET /upload-url verbatim; it already embeds a fresh <id>/<uuid> key under the base dir
- If building the path manually, keep {dir} a single safe segment: no '..', '/', backslash, NUL, or absolute path, and percent-encode it with url.PathEscape
- If you operate the server, keep STORE_DIR absolute (startup enforces this) and treat repeated 'Path traversal attempt blocked' warns as probing (rate-limit the source)
Example fix
// before (rejected with 400 invalid path)
PUT /upload/..%2F..%2Fetc/cron.d/pwn
// after: fetch an upload URL and use it unchanged
curl -H 'x-nb-client: netbird' 'https://srv/upload-url?id=peer123'
// -> {"url":"https://srv/upload/peer123/<uuid>","key":"peer123/<uuid>"}
curl -X PUT --data-binary @file 'https://srv/upload/peer123/<uuid>' Defensive patterns
Strategy: validation
Validate before calling
func safeSegment(s string) bool {
return s != "" && s != "." && s != ".." &&
!strings.Contains(s, "/") && !strings.Contains(s, "\\") && s == path.Base(path.Clean(s))
}
// before issuing the PUT
if !safeSegment(dir) || !safeSegment(file) {
return fmt.Errorf("unsafe upload path segment")
} Try / catch
After the PUT, if resp.StatusCode == 400 and the body reads 'invalid path', fix the client's path construction; do not blind-retry the same URL, it will fail identically.
Prevention
- Never hand-assemble the PUT path; always use the URL returned by GET /upload-url
- Reject any segment containing '/', backslash, '..', NUL, or absolute paths before sending
- Percent-encode path values with url.PathEscape so intermediaries cannot reinterpret them
When it happens
Trigger: PUT to /upload/{dir}/{file} where dir contains dot-dot segments or encoded separators that ServeMux decodes into PathValue, e.g. PUT /upload/..%2F..%2Fetc/cron.d/x, or a dir value that Join+Clean resolves outside the base directory.
Common situations: A client hand-crafting the PUT URL instead of using the one returned by GET /upload-url; a proxy that decodes %2E%2E or %2F before the request reaches Go's ServeMux; automated scanners probing for traversal.
Related errors
- file already exists
- id query param required
- unauthorized
- failed to verify signature of artifact keys
- no keys found in bundle
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/fe170fa0289e4914.
Report an issue: GitHub.