From 4c3e7c15519d038a045d82705b6f32096d8676e4 Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Sun, 11 Oct 2026 16:29:40 +0000 Subject: [PATCH] fix: check the CrowdSec diagnostics paths against the data folder properly The CrowdSec diagnostics now confirm the config and acquisition files are really inside the data folder, so a neighbouring folder whose name merely starts the same way is no longer accepted. Reuses the existing path containment helper, and its tests now also cover names such as "..foo" being accepted while real parent traversal is rejected. Closes #1541 Co-authored-by: AdamMagued --- backend/internal/api/handlers/crowdsec_files_test.go | 6 ++++++ backend/internal/api/handlers/crowdsec_handler.go | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/internal/api/handlers/crowdsec_files_test.go b/backend/internal/api/handlers/crowdsec_files_test.go index db25481d0..0c98cd3fa 100644 --- a/backend/internal/api/handlers/crowdsec_files_test.go +++ b/backend/internal/api/handlers/crowdsec_files_test.go @@ -578,6 +578,12 @@ func TestPathWithin(t *testing.T) { assert.False(t, pathWithin("/a/b", "/a")) assert.False(t, pathWithin("/a/b", "/x/y")) assert.False(t, pathWithin("/a/b", "relative")) + // A name that merely starts with two dots is inside the folder; real parent traversal is not. + assert.True(t, pathWithin("/a/b", "/a/b/..foo")) + assert.True(t, pathWithin("/a/b", "/a/b/..foo/bar")) + assert.False(t, pathWithin("/a/b", "/a/b/../c")) + assert.False(t, pathWithin("/a/b", "/a/b/../../etc/passwd")) + assert.False(t, pathWithin("/a/b", "/a/b_sibling/config.yaml")) } func TestCrowdsecFiles_Read_UnreadableFileIs500(t *testing.T) { diff --git a/backend/internal/api/handlers/crowdsec_handler.go b/backend/internal/api/handlers/crowdsec_handler.go index 378609aeb..64671ff8b 100644 --- a/backend/internal/api/handlers/crowdsec_handler.go +++ b/backend/internal/api/handlers/crowdsec_handler.go @@ -2216,7 +2216,7 @@ func (h *CrowdsecHandler) DiagnosticsConfig(c *gin.Context) { if configPath := crowdsec.FindConfigFile(h.DataDir); configPath != "" { // Path traversal protection: ensure path is within DataDir cleanConfigPath = filepath.Clean(configPath) - if !strings.HasPrefix(cleanConfigPath, filepath.Clean(h.DataDir)) { + if !pathWithin(filepath.Clean(h.DataDir), cleanConfigPath) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid config path"}) return } @@ -2262,7 +2262,7 @@ func (h *CrowdsecHandler) DiagnosticsConfig(c *gin.Context) { cleanDataDir := filepath.Clean(h.DataDir) // Path traversal protection cleanAcquisPath := filepath.Clean(acquisPath) - if !strings.HasPrefix(cleanAcquisPath, cleanDataDir) { + if !pathWithin(cleanDataDir, cleanAcquisPath) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid acquis path"}) return }