Skip to content
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions backend/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"

Expand Down Expand Up @@ -126,13 +127,33 @@
}

func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := "." + r.URL.Path
if _, err := os.Stat(h.staticDir + "/" + r.URL.Path); os.IsNotExist(err) {
staticAbs, err := filepath.Abs(h.staticDir)
if err != nil {
// If we cannot resolve the static directory, fall back to the SPA shell.
http.ServeFile(w, r, h.staticDir+"/index.html")
return
}

requestedPath := filepath.Join(staticAbs, r.URL.Path)
requestedAbs, err := filepath.Abs(requestedPath)
if err != nil {
// On error resolving the requested path, fall back to the SPA shell.
http.ServeFile(w, r, h.staticDir+"/index.html")
return
}

// Ensure the requested path is within the static directory to prevent directory traversal.
if len(requestedAbs) < len(staticAbs) || requestedAbs[:len(staticAbs)] != staticAbs {
http.ServeFile(w, r, h.staticDir+"/index.html")
return
}

if _, err := os.Stat(requestedAbs); os.IsNotExist(err) {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 5 days ago

To fix this, keep using absolute, normalized paths but strengthen the containment check between requestedAbs and staticAbs in a clear, standard way. Specifically, use strings.HasPrefix and ensure that either requestedAbs is exactly staticAbs or that it starts with staticAbs followed by the OS-specific path separator. This guarantees that /var/www/static2 is not treated as contained in /var/www/static, and it makes the intent explicit. Additionally, use filepath.Join instead of concatenating h.staticDir + "/index.html" so that path construction is correct and OS-independent.

Concretely, in backend/cmd/main.go:

  • Import the standard library package strings.
  • In spaHandler.ServeHTTP:
    • Compute indexFile := filepath.Join(staticAbs, "index.html") (after you know staticAbs), and reuse it for all fallback calls to http.ServeFile.
    • Replace the manual prefix check len(requestedAbs) < len(staticAbs) || requestedAbs[:len(staticAbs)] != staticAbs with a more robust boundary-aware prefix check using strings.HasPrefix and string(os.PathSeparator).

This preserves existing behavior (serving static files when inside the designated directory and index.html otherwise) while removing the path traversal risk and aligning with common secure path-handling patterns.

Suggested changeset 1
backend/cmd/main.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/backend/cmd/main.go b/backend/cmd/main.go
--- a/backend/cmd/main.go
+++ b/backend/cmd/main.go
@@ -17,6 +17,7 @@
 	"os"
 	"os/signal"
 	"path/filepath"
+	"strings"
 	"syscall"
 	"time"
 
@@ -130,27 +131,29 @@
 	staticAbs, err := filepath.Abs(h.staticDir)
 	if err != nil {
 		// If we cannot resolve the static directory, fall back to the SPA shell.
-		http.ServeFile(w, r, h.staticDir+"/index.html")
+		http.ServeFile(w, r, filepath.Join(h.staticDir, "index.html"))
 		return
 	}
 
+	indexFile := filepath.Join(staticAbs, "index.html")
+
 	requestedPath := filepath.Join(staticAbs, r.URL.Path)
 	requestedAbs, err := filepath.Abs(requestedPath)
 	if err != nil {
 		// On error resolving the requested path, fall back to the SPA shell.
-		http.ServeFile(w, r, h.staticDir+"/index.html")
+		http.ServeFile(w, r, indexFile)
 		return
 	}
 
 	// Ensure the requested path is within the static directory to prevent directory traversal.
-	if len(requestedAbs) < len(staticAbs) || requestedAbs[:len(staticAbs)] != staticAbs {
-		http.ServeFile(w, r, h.staticDir+"/index.html")
+	if !(requestedAbs == staticAbs || strings.HasPrefix(requestedAbs, staticAbs+string(os.PathSeparator))) {
+		http.ServeFile(w, r, indexFile)
 		return
 	}
 
 	if _, err := os.Stat(requestedAbs); os.IsNotExist(err) {
 		// Not a real file – serve the SPA shell
-		http.ServeFile(w, r, h.staticDir+"/index.html")
+		http.ServeFile(w, r, indexFile)
 		return
 	}
 
EOF
@@ -17,6 +17,7 @@
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"

@@ -130,27 +131,29 @@
staticAbs, err := filepath.Abs(h.staticDir)
if err != nil {
// If we cannot resolve the static directory, fall back to the SPA shell.
http.ServeFile(w, r, h.staticDir+"/index.html")
http.ServeFile(w, r, filepath.Join(h.staticDir, "index.html"))
return
}

indexFile := filepath.Join(staticAbs, "index.html")

requestedPath := filepath.Join(staticAbs, r.URL.Path)
requestedAbs, err := filepath.Abs(requestedPath)
if err != nil {
// On error resolving the requested path, fall back to the SPA shell.
http.ServeFile(w, r, h.staticDir+"/index.html")
http.ServeFile(w, r, indexFile)
return
}

// Ensure the requested path is within the static directory to prevent directory traversal.
if len(requestedAbs) < len(staticAbs) || requestedAbs[:len(staticAbs)] != staticAbs {
http.ServeFile(w, r, h.staticDir+"/index.html")
if !(requestedAbs == staticAbs || strings.HasPrefix(requestedAbs, staticAbs+string(os.PathSeparator))) {
http.ServeFile(w, r, indexFile)
return
}

if _, err := os.Stat(requestedAbs); os.IsNotExist(err) {
// Not a real file – serve the SPA shell
http.ServeFile(w, r, h.staticDir+"/index.html")
http.ServeFile(w, r, indexFile)
return
}

Copilot is powered by AI and may make mistakes. Always verify output.
@Bajahaw Bajahaw committed this autofix suggestion 5 days ago.
// Not a real file – serve the SPA shell
http.ServeFile(w, r, h.staticDir+"/index.html")
return
}
_ = path

h.fs.ServeHTTP(w, r)
}

Expand Down
Loading