From d3d7b695ff7f9d8857431cdcff1c50ab8b9b07cb Mon Sep 17 00:00:00 2001 From: Patrick D'appollonio <930925+patrickdappollonio@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:31:30 -0400 Subject: [PATCH] Fix CI lint failures (gosec G710) and bump dependencies golangci-lint's latest release added gosec rule G710 (open redirect via taint analysis), which broke the "Testing push" workflow on master and on every dependabot branch. Two of the three findings were legitimate: a request path starting with "//" would produce a scheme-relative Location header, redirecting to another host. Fix by collapsing duplicate leading slashes before redirecting, and suppress the remaining intentional case (operator-configured redirect rules may target external URLs by design). Also: - Bump github.com/yuin/goldmark 1.8.2 -> 1.8.4 - Bump golang.org/x/crypto 0.49.0 -> 0.52.0 - Bump github.com/go-chi/chi/v5 5.2.5 -> 5.3.1 - Bump github.com/klauspost/compress 1.18.6 -> 1.19.0 - Bump github.com/go-playground/validator/v10 10.30.2 -> 10.30.3 - Bump actions/checkout v6 -> v7 in all workflows - Migrate gomodguard -> gomodguard_v2 (deprecation warning) Co-Authored-By: Claude Fable 5 --- .github/workflows/releasing.yaml | 2 +- .github/workflows/testing-commit.yaml | 2 +- .github/workflows/testing-pull-request.yaml | 2 +- .golangci.yaml | 2 +- go.mod | 14 +++++------ go.sum | 28 ++++++++++----------- internal/middlewares/redir_indexes.go | 10 +++++++- internal/redirects/redirect.go | 9 ++++++- internal/server/handlers.go | 10 +++++++- 9 files changed, 51 insertions(+), 28 deletions(-) diff --git a/.github/workflows/releasing.yaml b/.github/workflows/releasing.yaml index 1dbfacf..e66f9ba 100644 --- a/.github/workflows/releasing.yaml +++ b/.github/workflows/releasing.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Set up Go diff --git a/.github/workflows/testing-commit.yaml b/.github/workflows/testing-commit.yaml index cdf7428..1855211 100644 --- a/.github/workflows/testing-commit.yaml +++ b/.github/workflows/testing-commit.yaml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Set up Go diff --git a/.github/workflows/testing-pull-request.yaml b/.github/workflows/testing-pull-request.yaml index ddc344d..c73d39d 100644 --- a/.github/workflows/testing-pull-request.yaml +++ b/.github/workflows/testing-pull-request.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Set up Go diff --git a/.golangci.yaml b/.golangci.yaml index 094ed2d..7a72bec 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -26,7 +26,7 @@ linters: - gocritic - gocyclo - goheader - - gomodguard + - gomodguard_v2 - goprintffuncname - gosec - gosmopolitan diff --git a/go.mod b/go.mod index 9398b8a..b489aed 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,15 @@ module github.com/patrickdappollonio/http-server go 1.25.0 require ( - github.com/go-chi/chi/v5 v5.2.5 - github.com/go-playground/validator/v10 v10.30.2 + github.com/go-chi/chi/v5 v5.3.1 + github.com/go-playground/validator/v10 v10.30.3 github.com/golang-jwt/jwt/v5 v5.3.1 - github.com/klauspost/compress v1.18.6 + github.com/klauspost/compress v1.19.0 github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 - github.com/yuin/goldmark v1.8.2 + github.com/yuin/goldmark v1.8.4 go.abhg.dev/goldmark/mermaid v0.6.0 go.uber.org/automaxprocs v1.6.0 ) @@ -30,8 +30,8 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.35.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 1f9a4da..f973384 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= -github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8= +github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs= github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -23,8 +23,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= -github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= +github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8= +github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -39,8 +39,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= -github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= +github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -78,20 +78,20 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= -github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA= +github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.abhg.dev/goldmark/mermaid v0.6.0 h1:VvkYFWuOjD6cmSBVJpLAtzpVCGM1h0B7/DQ9IzERwzY= go.abhg.dev/goldmark/mermaid v0.6.0/go.mod h1:uMc+PcnIH2NVL7zjH10Q1wr7hL3+4n4jUMifhyBYB9I= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/middlewares/redir_indexes.go b/internal/middlewares/redir_indexes.go index dd34855..4e6585e 100644 --- a/internal/middlewares/redir_indexes.go +++ b/internal/middlewares/redir_indexes.go @@ -16,7 +16,15 @@ func RedirectIndexes(statusCode int) func(http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for _, index := range indexes { if strings.HasSuffix(r.URL.Path, index) { - http.Redirect(w, r, strings.TrimSuffix(r.URL.Path, index), statusCode) + target := strings.TrimSuffix(r.URL.Path, index) + + // Collapse duplicate leading slashes so the target can't be + // interpreted by browsers as a scheme-relative URL ("//evil.com/"). + if strings.HasPrefix(target, "//") { + target = "/" + strings.TrimLeft(target, "/") + } + + http.Redirect(w, r, target, statusCode) //nolint:gosec // target is derived from the request path with leading slashes collapsed, so it is always same-origin return } } diff --git a/internal/redirects/redirect.go b/internal/redirects/redirect.go index 5aee5fa..2cc0377 100644 --- a/internal/redirects/redirect.go +++ b/internal/redirects/redirect.go @@ -50,7 +50,7 @@ func (e *Engine) Middleware(logger io.Writer) func(http.Handler) http.Handler { } fmt.Fprintf(logger, "REDIR %q -> %q (status: %d)\n", r.URL.RequestURI(), destination, statusCode) - http.Redirect(w, r, destination, statusCode) + http.Redirect(w, r, destination, statusCode) //nolint:gosec // destinations come from the operator-provided redirects file; redirecting to external URLs is an intended feature }) } } @@ -387,6 +387,13 @@ func (rule *RedirectRule) buildDestination(params map[string]string, requestRawQ destination = destURL.String() } + // If the rule targets a local path, make sure placeholder expansion can't + // turn it into a scheme-relative URL ("//evil.com") by collapsing any + // duplicate leading slashes introduced by captured values. + if strings.HasPrefix(rule.To, "/") && !strings.HasPrefix(rule.To, "//") && strings.HasPrefix(destination, "//") { + destination = "/" + strings.TrimLeft(destination, "/") + } + return destination } diff --git a/internal/server/handlers.go b/internal/server/handlers.go index cbc1ec6..586c344 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -73,7 +73,15 @@ func (s *Server) showOrRender(w http.ResponseWriter, r *http.Request) { if info.IsDir() { // Check if the path doesn't ends in a slash, and redirect accordingly if !strings.HasSuffix(r.URL.Path, "/") { - http.Redirect(w, r, r.URL.Path+"/", http.StatusMovedPermanently) + target := r.URL.Path + "/" + + // Collapse duplicate leading slashes so the target can't be + // interpreted by browsers as a scheme-relative URL ("//evil.com/"). + if strings.HasPrefix(target, "//") { + target = "/" + strings.TrimLeft(target, "/") + } + + http.Redirect(w, r, target, http.StatusMovedPermanently) //nolint:gosec // target is derived from the request path with leading slashes collapsed, so it is always same-origin return }