mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-18 00:59:38 +00:00
Replace strings.Split with the lazy strings.SplitSeq iterator (Go 1.23+) in loops that only range over the tokens, avoiding the intermediate slice allocation. The yielded substrings are identical to Split, so behavior is unchanged. Applies the suggestion from #5599 to its three sites (CIDR parsing, WebDAV hidden-path, overlay path checks) and extends it to two more split-and-iterate loops in the server compression negotiator and MCP config-options builder for consistency. The slice returning splitPath helper is intentionally left on strings.Split.
199 lines
5.4 KiB
Go
199 lines
5.4 KiB
Go
package security
|
|
|
|
import (
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/photoprism/photoprism/pkg/fs"
|
|
)
|
|
|
|
// overlayBlockedFileNames lists sensitive file base names that must never be
|
|
// served by the web overlay, even when present under /storage/web.
|
|
var overlayBlockedFileNames = map[string]struct{}{
|
|
"id_ed25519": {},
|
|
"id_ed25519.pub": {},
|
|
"id_rsa": {},
|
|
"id_rsa.pub": {},
|
|
"config.toml": {},
|
|
"config.yaml": {},
|
|
"config.yml": {},
|
|
"defaults.yaml": {},
|
|
"defaults.yml": {},
|
|
"options.yaml": {},
|
|
"options.yml": {},
|
|
"hub.yml": {},
|
|
"hub.yaml": {},
|
|
"auth.json": {},
|
|
"join_token": {},
|
|
"client_secret": {},
|
|
}
|
|
|
|
// overlayBlockedFileExt lists sensitive file extensions that must never be
|
|
// served by the web overlay, even when present under /storage/web.
|
|
var overlayBlockedFileExt = map[string]struct{}{
|
|
".key": {},
|
|
".rsa": {},
|
|
".jwk": {},
|
|
".pem": {},
|
|
".sql": {},
|
|
".toml": {},
|
|
}
|
|
|
|
// overlayBlockedPathPrefixes lists sensitive slash paths rooted under the
|
|
// overlay tree that must always be denied (path-prefix match).
|
|
var overlayBlockedPathPrefixes = []string{
|
|
"node/secrets",
|
|
"config/portal",
|
|
"config/certificates",
|
|
}
|
|
|
|
// OverlayHasAmbiguousPath returns true when a request path is unsafe due to
|
|
// ambiguity, traversal indicators, hidden/special segments, or encoded probes.
|
|
func OverlayHasAmbiguousPath(requestPath, escapedPath string) bool {
|
|
if requestPath == "" {
|
|
return true
|
|
}
|
|
|
|
if strings.Contains(requestPath, "\\") || strings.Contains(requestPath, "//") {
|
|
return true
|
|
}
|
|
|
|
if strings.Contains(requestPath, "/./") || strings.Contains(requestPath, "/../") ||
|
|
strings.HasSuffix(requestPath, "/.") || strings.HasSuffix(requestPath, "/..") {
|
|
return true
|
|
}
|
|
|
|
cleanPath := path.Clean(requestPath)
|
|
if cleanPath == "." {
|
|
return true
|
|
} else if !strings.HasPrefix(cleanPath, "/") {
|
|
cleanPath = "/" + cleanPath
|
|
}
|
|
|
|
// Allow only trailing slash normalization; reject all other material changes.
|
|
if requestPath != cleanPath && requestPath != cleanPath+"/" {
|
|
return true
|
|
}
|
|
|
|
if relPath := strings.Trim(cleanPath, "/"); relPath != "" {
|
|
for segment := range strings.SplitSeq(relPath, "/") {
|
|
if segment == "" || segment == "." || segment == ".." || fs.FileNameHidden(segment) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
lowerEscaped := strings.ToLower(escapedPath)
|
|
return strings.Contains(lowerEscaped, "%2e") || strings.Contains(lowerEscaped, "%2f") || strings.Contains(lowerEscaped, "%5c")
|
|
}
|
|
|
|
// OverlayRelativePath maps a request path to an overlay-relative path while
|
|
// enforcing the configured base path scope.
|
|
func OverlayRelativePath(requestPath, webBase string) (string, bool) {
|
|
if requestPath == "" {
|
|
return "", false
|
|
}
|
|
|
|
absPath := strings.Trim(strings.ReplaceAll(requestPath, "\\", "/"), "/")
|
|
|
|
switch {
|
|
case webBase != "" && absPath == webBase:
|
|
// Request targets the base path itself (e.g. "/i/acme"), so map it to index.
|
|
return "", true
|
|
case webBase != "" && strings.HasPrefix(absPath, webBase+"/"):
|
|
// Request is under the configured base path; strip the prefix for web storage lookup.
|
|
return strings.TrimPrefix(absPath, webBase+"/"), true
|
|
case webBase != "":
|
|
// Ignore requests outside the configured base path when a base path is enforced.
|
|
return "", false
|
|
default:
|
|
return absPath, true
|
|
}
|
|
}
|
|
|
|
// OverlayPathBlocked returns true when an overlay path targets hidden/special
|
|
// path segments or files and directories blocked by denylist rules.
|
|
func OverlayPathBlocked(webPath string) bool {
|
|
relPath := strings.Trim(strings.ReplaceAll(webPath, "\\", "/"), "/")
|
|
if relPath == "" {
|
|
return false
|
|
}
|
|
|
|
segments := strings.SplitSeq(relPath, "/")
|
|
|
|
for segment := range segments {
|
|
if segment == "" || fs.FileNameHidden(segment) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
lowerPath := strings.ToLower(relPath)
|
|
lowerBase := path.Base(lowerPath)
|
|
|
|
// Block requests for sensitive file names like "auth.json".
|
|
if _, blocked := overlayBlockedFileNames[lowerBase]; blocked {
|
|
return true
|
|
}
|
|
|
|
// Block file extensions commonly used for private keys or backups.
|
|
if _, blocked := overlayBlockedFileExt[path.Ext(lowerBase)]; blocked {
|
|
return true
|
|
}
|
|
|
|
// Additionally check for sensitive path prefixes that should not be served.
|
|
for _, prefix := range overlayBlockedPathPrefixes {
|
|
if lowerPath == prefix || strings.HasPrefix(lowerPath, prefix+"/") {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// OverlayResolveFile resolves an overlay file path and returns false when the
|
|
// target cannot be served safely from inside the canonical overlay root.
|
|
func OverlayResolveFile(webDir, webPath string) (string, bool) {
|
|
if webDir == "" || webPath == "" {
|
|
return "", false
|
|
}
|
|
|
|
overlayPath := strings.Trim(strings.ReplaceAll(webPath, "\\", "/"), "/")
|
|
if overlayPath == "" {
|
|
return "", false
|
|
}
|
|
|
|
candidate := filepath.Join(webDir, filepath.FromSlash(overlayPath))
|
|
if !fs.FileExists(candidate) {
|
|
return "", false
|
|
}
|
|
|
|
rootRealPath, err := filepath.EvalSymlinks(webDir)
|
|
if err != nil {
|
|
rootRealPath = webDir
|
|
}
|
|
|
|
rootAbsPath, err := filepath.Abs(rootRealPath)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
|
|
fileRealPath, err := filepath.EvalSymlinks(candidate)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
|
|
fileAbsPath, err := filepath.Abs(fileRealPath)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
|
|
relPath, err := filepath.Rel(rootAbsPath, fileAbsPath)
|
|
if err != nil {
|
|
return "", false
|
|
} else if relPath == ".." || strings.HasPrefix(relPath, ".."+string(filepath.Separator)) {
|
|
return "", false
|
|
}
|
|
|
|
return fileAbsPath, true
|
|
}
|