From dab3a424677eed494c1584050cb307081202014d Mon Sep 17 00:00:00 2001 From: vaggeliskls Date: Wed, 14 Oct 2026 12:32:14 +0300 Subject: [PATCH 1/2] fix(security): add image healthcheck and seccomp profile - Dockerfile: HEALTHCHECK using the built-in --healthcheck probe (DS-0026) - Helm: RuntimeDefault seccomp profile on the pod (KSV-0030, KSV-0104) - Security workflow: run on push to main instead of weekly schedule --- .github/workflows/security.yml | 4 ++-- Dockerfile | 4 ++++ kubernetes/templates/deployment.yaml | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c749ed8..38b0da7 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,8 +1,8 @@ name: Security on: - schedule: - - cron: "0 6 * * 1" # weekly: catch newly published CVEs in unchanged code + push: + branches: [main] workflow_dispatch: permissions: diff --git a/Dockerfile b/Dockerfile index b98c675..304a46a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,4 +26,8 @@ USER 1000:1000 EXPOSE 8080 +# Uses the binary's built-in probe; scratch has no shell or curl. +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD ["/webdav-server", "--healthcheck"] + ENTRYPOINT ["/webdav-server"] diff --git a/kubernetes/templates/deployment.yaml b/kubernetes/templates/deployment.yaml index b34764a..180c9c2 100644 --- a/kubernetes/templates/deployment.yaml +++ b/kubernetes/templates/deployment.yaml @@ -22,6 +22,8 @@ spec: runAsGroup: 1000 runAsNonRoot: true fsGroup: 1000 + seccompProfile: + type: RuntimeDefault containers: - name: webdav image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" From 2d6dfc047835356093867d0047fcc180ddbe6f10 Mon Sep 17 00:00:00 2001 From: vaggeliskls Date: Wed, 14 Oct 2026 12:37:55 +0300 Subject: [PATCH 2/2] fix(server): prevent open redirect on directory trailing-slash redirect A request for //evil.com where a /evil.com directory exists was redirected to the protocol-relative //evil.com/, sending browsers off-site. Redirect to a relative, escaped target instead so http.Redirect resolves and cleans it against the request path. Flagged by gosec G710 in golangci-lint v2.14. --- internal/server/dirlist.go | 5 +++- internal/server/dirlist_test.go | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 internal/server/dirlist_test.go diff --git a/internal/server/dirlist.go b/internal/server/dirlist.go index 9f74991..e2f3414 100644 --- a/internal/server/dirlist.go +++ b/internal/server/dirlist.go @@ -6,6 +6,7 @@ import ( "html/template" "log/slog" "net/http" + "net/url" "path" "sort" "strings" @@ -72,7 +73,9 @@ func (h *dirListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !strings.HasSuffix(urlPath, "/") { fi, err := h.fs.Stat(r.Context(), urlPath) if err == nil && fi.IsDir() { - http.Redirect(w, r, urlPath+"/", http.StatusMovedPermanently) + // Relative target: http.Redirect resolves it against the request + // path and cleans it, so "//host" can never become the Location. + http.Redirect(w, r, "./"+url.PathEscape(path.Base(urlPath))+"/", http.StatusMovedPermanently) return } } diff --git a/internal/server/dirlist_test.go b/internal/server/dirlist_test.go new file mode 100644 index 0000000..93fb956 --- /dev/null +++ b/internal/server/dirlist_test.go @@ -0,0 +1,44 @@ +package server + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "testing" + + "golang.org/x/net/webdav" +) + +func TestDirList_RedirectAddsTrailingSlash(t *testing.T) { + fs := webdav.NewMemFS() + for _, dir := range []string{"/files", "/files/sub dir", "/evil.com"} { + if err := fs.Mkdir(context.Background(), dir, os.ModePerm); err != nil { + t.Fatal(err) + } + } + h := &dirListHandler{webdav: &webdav.Handler{FileSystem: fs, LockSystem: webdav.NewMemLS()}, fs: fs} + + tests := []struct { + path string + want string + }{ + {"/files", "/files/"}, + {"/files/sub dir", "/files/sub%20dir/"}, + // Protocol-relative path must not redirect off-site. + {"//evil.com", "/evil.com/"}, + } + for _, tt := range tests { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.URL.Path = tt.path + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + + if rec.Code != http.StatusMovedPermanently { + t.Errorf("%q: status = %d, want %d", tt.path, rec.Code, http.StatusMovedPermanently) + } + if got := rec.Header().Get("Location"); got != tt.want { + t.Errorf("%q: Location = %q, want %q", tt.path, got, tt.want) + } + } +}