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 }