fix(security): close High/Critical findings from the Stage 8 scan
- Bump github.com/rabbitmq/amqp091-go 1.12.0 -> 1.13.0 (3 Critical, 7 High,
reachable via rabbitmq.Connect) and golang.org/x/{text,net,crypto},
klauspost/compress; toolchain go1.26.8, builder image pinned to
golang:1.26.8-alpine3.24.
- Build Content-Disposition per RFC 6266/8187 (ASCII fallback +
filename*=UTF-8''), stripping CR/LF, quotes and backslashes (CWE-113).
- Cap request bodies with MaxBytesReader (HTTP_MAX_UPLOAD_BYTES, default
2 GiB); multipart uploads over the limit return 413 (CWE-400).
- /readyz reports "unavailable" instead of raw backend errors (CWE-209).
- Drop deprecated middleware.RealIP; add X-Content-Type-Options: nosniff.
- docker-compose credentials come from ${VAR:-default}.
- Check the RegisterValidation error; annotate verified gosec false
positives with #nosec and a reason.
Re-scan: govulncheck 0 reachable, trivy fs/image 0 High/Critical,
gosec High 0. go test -race passes against PostGIS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
131
internal/transport/http/middleware_test.go
Normal file
131
internal/transport/http/middleware_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// multipartBody builds a multipart/form-data body with one file part of
|
||||
// size bytes.
|
||||
func multipartBody(t *testing.T, size int) (*bytes.Buffer, string) {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
part, err := mw.CreateFormFile("file", "big.tif")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := part.Write(bytes.Repeat([]byte{'x'}, size)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := mw.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &buf, mw.FormDataContentType()
|
||||
}
|
||||
|
||||
func TestLimitRequestBodyRejectsOversizedUpload(t *testing.T) {
|
||||
body, ct := multipartBody(t, 4096)
|
||||
req := httptest.NewRequest(http.MethodPost, "/landscape/slots/lp-z-lcr", body)
|
||||
req.Header.Set("Content-Type", ct)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
h := limitRequestBody(1024)(http.HandlerFunc(newLandscapeTestHandler().uploadSlot))
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusRequestEntityTooLarge {
|
||||
t.Fatalf("status = %d, want %d (body %s)", rec.Code, http.StatusRequestEntityTooLarge, rec.Body.String())
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "1024 byte limit") {
|
||||
t.Fatalf("body %s does not state the limit", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitRequestBodyPassesSmallBody(t *testing.T) {
|
||||
var got []byte
|
||||
h := limitRequestBody(1024)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
got, err = io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("read: %v", err)
|
||||
}
|
||||
}))
|
||||
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/", strings.NewReader("hello")))
|
||||
if string(got) != "hello" {
|
||||
t.Fatalf("body = %q, want hello", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitRequestBodyDisabled(t *testing.T) {
|
||||
h := limitRequestBody(0)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := io.ReadAll(r.Body); err != nil {
|
||||
t.Errorf("read with cap disabled: %v", err)
|
||||
}
|
||||
}))
|
||||
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(make([]byte, 1<<20))))
|
||||
}
|
||||
|
||||
func newTestRouter() http.Handler {
|
||||
v := validator.New(validator.WithRequiredStructEnabled())
|
||||
return NewRouter(RouterDeps{
|
||||
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||
Health: NewHealthHandler(nil, nil),
|
||||
Categories: NewCategoryHandler(nil, v),
|
||||
Datasets: NewDatasetHandler(nil, v),
|
||||
DatasetGroups: NewDatasetGroupHandler(nil, v),
|
||||
Landscape: NewLandscapeHandler(nil, v),
|
||||
MaxBodyBytes: 1024,
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouterSetsNosniff(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
newTestRouter().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil))
|
||||
if got := rec.Header().Get("X-Content-Type-Options"); got != "nosniff" {
|
||||
t.Fatalf("X-Content-Type-Options = %q, want nosniff", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterCapsUploadBody(t *testing.T) {
|
||||
body, ct := multipartBody(t, 4096)
|
||||
req := httptest.NewRequest(http.MethodPost, "/landscape/slots/lp-z-lcr", body)
|
||||
req.Header.Set("Content-Type", ct)
|
||||
rec := httptest.NewRecorder()
|
||||
newTestRouter().ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusRequestEntityTooLarge {
|
||||
t.Fatalf("status = %d, want %d (body %s)", rec.Code, http.StatusRequestEntityTooLarge, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadyDoesNotLeakCheckErrors(t *testing.T) {
|
||||
h := NewHealthHandler(map[string]ReadinessCheck{
|
||||
"postgres": func(context.Context) error {
|
||||
return errors.New("dial tcp 10.1.2.3:5432: connect: connection refused (user=gis)")
|
||||
},
|
||||
"s3": func(context.Context) error { return nil },
|
||||
}, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.Ready(rec, httptest.NewRequest(http.MethodGet, "/readyz", nil))
|
||||
|
||||
if rec.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status = %d, want %d", rec.Code, http.StatusServiceUnavailable)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if strings.Contains(body, "10.1.2.3") || strings.Contains(body, "dial tcp") {
|
||||
t.Fatalf("readiness body leaks internal error details: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"postgres":"unavailable"`) || !strings.Contains(body, `"s3":"ok"`) {
|
||||
t.Fatalf("unexpected readiness body: %s", body)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user