mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-20 14:17:07 +00:00
* s3,iceberg: reject `..`/NUL in URL path vars Both gateway routers use mux.NewRouter().SkipClean(true), so a request like `GET /bucket-A/../evil-bucket/key` survives routing as bucket=bucket-A, object=../evil-bucket/key. The captured key is then joined into a filer path; util.JoinPath / path.Join collapse the `..` server-side and the read lands in evil-bucket. With auth on, IAM still authorizes against bucket-A (the mux var), so policy is evaluated against the wrong target. Add a middleware on the S3 bucket subrouter and the Iceberg REST router that rejects any `.`, `..`, NUL, or — for single-segment slots — embedded slash in the captured path vars before any handler runs. NormalizeObjectKey already folds `\` to `/` and decoding happens in mux, so `%2e%2e` and `..\` are caught. * s3,iceberg: reject empty captured vars and empty namespace parts Comma-ok the var lookup so we only check captured slots, then treat an empty captured value as a rejection on its own — downstream path.Join would otherwise collapse it and let the next segment pick the bucket. For iceberg, also reject empty parts after splitting the namespace on \x1F so leading/trailing/consecutive unit separators (which parseNamespace silently folds out) don't let distinct route values collapse to the same parsed namespace. Register loggingMiddleware before validateRequestPath on the iceberg router so rejected requests still produce an audit-log line.
71 lines
2.4 KiB
Go
71 lines
2.4 KiB
Go
package iceberg
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
)
|
|
|
|
// validateRequestPath rejects Iceberg REST requests whose captured
|
|
// {prefix}/{namespace}/{table} mux vars would produce a parent-directory
|
|
// traversal when joined into a filer path. The iceberg router runs with
|
|
// SkipClean(true), so `..` survives routing; downstream path.Join calls
|
|
// (stageCreateMarkerDir, location builders, etc.) then collapse it and
|
|
// escape the table-bucket directory.
|
|
//
|
|
// {prefix} maps to a table-bucket name; {table} is a single path segment;
|
|
// {namespace} is unit-separator (0x1F) joined parts that get flattened into
|
|
// a single dotted name for the on-disk layout — each part is validated
|
|
// individually.
|
|
func validateRequestPath(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
// Use the comma-ok form so vars only checked when the matched route
|
|
// actually captures them; when captured, an empty value is itself a
|
|
// rejection because downstream path.Join would collapse it.
|
|
if prefix, ok := vars["prefix"]; ok {
|
|
if prefix == "" || !s3_constants.IsValidBucketName(prefix) {
|
|
writeError(w, http.StatusBadRequest, "BadRequest", "invalid prefix")
|
|
return
|
|
}
|
|
}
|
|
if table, ok := vars["table"]; ok {
|
|
if table == "" || !isValidNameSegment(table) {
|
|
writeError(w, http.StatusBadRequest, "BadRequest", "invalid table name")
|
|
return
|
|
}
|
|
}
|
|
if ns, ok := vars["namespace"]; ok {
|
|
if ns == "" {
|
|
writeError(w, http.StatusBadRequest, "BadRequest", "invalid namespace")
|
|
return
|
|
}
|
|
// Reject leading/trailing/consecutive unit separators so distinct
|
|
// inputs cannot collapse to the same parsed namespace via
|
|
// parseNamespace's empty-part filter.
|
|
for _, part := range strings.Split(ns, "\x1F") {
|
|
if part == "" || !isValidNameSegment(part) {
|
|
writeError(w, http.StatusBadRequest, "BadRequest", "invalid namespace")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// isValidNameSegment rejects a single path-segment value (bucket prefix slot,
|
|
// table name, or one namespace part) that would be unsafe to embed in a filer
|
|
// path: `.`, `..`, embedded slash/backslash, or NUL.
|
|
func isValidNameSegment(s string) bool {
|
|
if s == "" {
|
|
return true
|
|
}
|
|
if s == "." || s == ".." {
|
|
return false
|
|
}
|
|
return !strings.ContainsAny(s, "/\\\x00")
|
|
}
|