[mirror] Scalable static site server for Git forges (like GitHub Pages)

Make project name validation more consistent and stricter.

Previously, you could issue e.g. a `GET /%2e%2e/%2e%2e` and it would
get interpreted as a parent directory path segment in the handler.
This didn't result in a path traversal vulnerability when passed to
the S3 backend because of a `path.Clean()` call indirectly done by
`makeWebRoot()`, but it's prudent to not take chances.

Changed files
+18 -12
src
+5 -1
src/auth.go
··· 65 65 return host, nil 66 66 } 67 67 68 + func IsValidProjectName(name string) bool { 69 + return !strings.HasPrefix(name, ".") && !strings.Contains(name, "%") 70 + } 71 + 68 72 func GetProjectName(r *http.Request) (string, error) { 69 73 // path must be either `/` or `/foo/` (`/foo` is accepted as an alias) 70 74 path := strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, "/"), "/") 71 - if path == ".index" || strings.HasPrefix(path, ".index/") { 75 + if !IsValidProjectName(path) { 72 76 return "", AuthError{http.StatusBadRequest, 73 77 fmt.Sprintf("directory name %q is reserved", ".index")} 74 78 } else if strings.Contains(path, "/") {
+13 -11
src/pages.go
··· 132 132 err = nil 133 133 sitePath = strings.TrimPrefix(r.URL.Path, "/") 134 134 if projectName, projectPath, hasProjectSlash := strings.Cut(sitePath, "/"); projectName != "" { 135 - var projectManifest *Manifest 136 - var projectMetadata ManifestMetadata 137 - projectManifest, projectMetadata, err = backend.GetManifest( 138 - r.Context(), makeWebRoot(host, projectName), 139 - GetManifestOptions{BypassCache: bypassCache}, 140 - ) 141 - if err == nil { 142 - if !hasProjectSlash { 143 - writeRedirect(w, http.StatusFound, r.URL.Path+"/") 144 - return nil 135 + if IsValidProjectName(projectName) { 136 + var projectManifest *Manifest 137 + var projectMetadata ManifestMetadata 138 + projectManifest, projectMetadata, err = backend.GetManifest( 139 + r.Context(), makeWebRoot(host, projectName), 140 + GetManifestOptions{BypassCache: bypassCache}, 141 + ) 142 + if err == nil { 143 + if !hasProjectSlash { 144 + writeRedirect(w, http.StatusFound, r.URL.Path+"/") 145 + return nil 146 + } 147 + sitePath, manifest, metadata = projectPath, projectManifest, projectMetadata 145 148 } 146 - sitePath, manifest, metadata = projectPath, projectManifest, projectMetadata 147 149 } 148 150 } 149 151 if manifest == nil && (err == nil || errors.Is(err, ErrObjectNotFound)) {