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:
2026-10-15 21:44:10 +05:00
parent 2544d4e146
commit 5b44cbda2f
22 changed files with 505 additions and 83 deletions

View File

@@ -0,0 +1,57 @@
package http
import (
"mime"
"strings"
"testing"
)
func TestContentDisposition(t *testing.T) {
tests := []struct {
name string
filename string
want string
}{
{"plain", "roads.geojson", `attachment; filename="roads.geojson"; filename*=UTF-8''roads.geojson`},
{"space", "my map.tif", `attachment; filename="my map.tif"; filename*=UTF-8''my%20map.tif`},
{"quote injection", `a.tif"; filename="evil.exe`, `attachment; filename="a.tif_; filename=_evil.exe"; filename*=UTF-8''a.tif%22%3B%20filename%3D%22evil.exe`},
{"crlf injection", "a.tif\r\nSet-Cookie: x=1", `attachment; filename="a.tifSet-Cookie: x=1"; filename*=UTF-8''a.tifSet-Cookie%3A%20x%3D1`},
{"backslash", `..\..\a.sld`, `attachment; filename=".._.._a.sld"; filename*=UTF-8''..%5C..%5Ca.sld`},
{"cyrillic", "карта.tif", `attachment; filename="_____.tif"; filename*=UTF-8''%D0%BA%D0%B0%D1%80%D1%82%D0%B0.tif`},
{"empty", "", `attachment; filename="download"; filename*=UTF-8''download`},
{"only controls", "\r\n\t", `attachment; filename="download"; filename*=UTF-8''download`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := contentDisposition("attachment", tt.filename)
if got != tt.want {
t.Fatalf("contentDisposition(%q)\n got %s\nwant %s", tt.filename, got, tt.want)
}
if strings.ContainsAny(got, "\r\n") {
t.Fatalf("header value contains CR/LF: %q", got)
}
// The value must parse as a single disposition whose decoded
// filename is the sanitised original.
disp, params, err := mime.ParseMediaType(got)
if err != nil {
t.Fatalf("ParseMediaType(%q): %v", got, err)
}
if disp != "attachment" {
t.Fatalf("disposition = %q, want attachment", disp)
}
if len(params) != 1 {
t.Fatalf("params = %v, want exactly one decoded filename", params)
}
})
}
}
func TestContentDispositionDecodesUTF8Name(t *testing.T) {
_, params, err := mime.ParseMediaType(contentDisposition("inline", "карта 2026.cog.tif"))
if err != nil {
t.Fatal(err)
}
if got := params["filename"]; got != "карта 2026.cog.tif" {
t.Fatalf("decoded filename = %q", got)
}
}