Merge pull request #481 from CJackHwang/dev

[codex] fix WebUI static root path guard
This commit is contained in:
CJACK.
2026-05-10 18:59:23 +08:00
committed by GitHub
2 changed files with 34 additions and 1 deletions

View File

@@ -100,7 +100,7 @@ func (h *Handler) serveFromDisk(w http.ResponseWriter, r *http.Request, staticDi
path = strings.TrimPrefix(path, "/")
if path != "" && strings.Contains(path, ".") {
full := filepath.Join(root, filepath.Clean(path))
if full != root && !strings.HasPrefix(full, root+string(os.PathSeparator)) {
if !isPathInsideRoot(full, root) {
http.NotFound(w, r)
return
}
@@ -127,6 +127,20 @@ func (h *Handler) serveFromDisk(w http.ResponseWriter, r *http.Request, staticDi
http.ServeFile(w, r, index)
}
func isPathInsideRoot(path, root string) bool {
cleanPath := filepath.Clean(path)
cleanRoot := filepath.Clean(root)
if cleanPath == cleanRoot {
return true
}
volume := filepath.VolumeName(cleanRoot)
rootWithoutVolume := cleanRoot[len(volume):]
if rootWithoutVolume == string(os.PathSeparator) {
return strings.HasPrefix(cleanPath, cleanRoot)
}
return strings.HasPrefix(cleanPath, cleanRoot+string(os.PathSeparator))
}
func resolveStaticAdminDir(preferred string) string {
if strings.TrimSpace(os.Getenv("DS2API_STATIC_ADMIN_DIR")) != "" {
return filepath.Clean(preferred)

View File

@@ -105,6 +105,25 @@ func TestServeFromDiskRejectsSiblingDirectoryWithSharedPrefix(t *testing.T) {
}
}
func TestIsPathInsideRootAllowsFilesystemRootChildren(t *testing.T) {
root := filepath.VolumeName(os.TempDir()) + string(os.PathSeparator)
child := filepath.Join(root, "assets", "index.css")
if !isPathInsideRoot(child, root) {
t.Fatalf("expected filesystem-root child %q inside %q", child, root)
}
}
func TestIsPathInsideRootRejectsSharedPrefixSibling(t *testing.T) {
parent := t.TempDir()
root := filepath.Join(parent, "admin")
sibling := filepath.Join(parent, "admin-leak", "secret.txt")
if isPathInsideRoot(sibling, root) {
t.Fatalf("expected shared-prefix sibling %q outside %q", sibling, root)
}
}
// TestSetStaticContentTypeUnknownExtensionFallsThrough verifies that unknown
// extensions leave the Content-Type header unset, so http.ServeFile can apply
// its own detection (sniffing or mime.TypeByExtension) for cases the pinned