package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "regexp" "strings" "testing" "github.com/Wikid82/charon/backend/internal/api/middleware" "github.com/Wikid82/charon/backend/internal/logger" "github.com/gin-gonic/gin" ) func TestImportUploadSanitizesFilename(t *testing.T) { gin.SetMode(gin.TestMode) tmpDir := t.TempDir() // set up in-memory DB for handler db := OpenTestDB(t) // Create a fake caddy executable to avoid dependency on system binary fakeCaddy := filepath.Join(tmpDir, "caddy") _ = os.WriteFile(fakeCaddy, []byte("#!/bin/sh\nexit 0"), 0o755) svc := NewImportHandler(db, fakeCaddy, tmpDir, "") router := gin.New() router.Use(middleware.RequestID()) router.POST("/import/upload", svc.Upload) buf := &bytes.Buffer{} logger.Init(true, buf) maliciousFilename := "../evil\nfile.caddy" payload := map[string]any{"filename": maliciousFilename, "content": "site { respond \"ok\" }"} bodyBytes, _ := json.Marshal(payload) req := httptest.NewRequest(http.MethodPost, "/import/upload", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) out := buf.String() // Extract the logged filename from either text or JSON log format textRegex := regexp.MustCompile(`filename=?"?([^"\s]*)"?`) jsonRegex := regexp.MustCompile(`"filename":"([^"]*)"`) var loggedFilename string if m := textRegex.FindStringSubmatch(out); len(m) == 2 { loggedFilename = m[1] } else if m := jsonRegex.FindStringSubmatch(out); len(m) == 2 { loggedFilename = m[1] } else { // if we can't extract a filename value, fail the test t.Fatalf("could not extract filename from logs: %s", out) } if strings.Contains(loggedFilename, "\n") || strings.Contains(loggedFilename, "\r") { t.Fatalf("log filename contained raw newline: %q", loggedFilename) } if strings.Contains(loggedFilename, "..") { t.Fatalf("log filename contained path traversal: %q", loggedFilename) } }