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:
57
internal/transport/http/content_disposition_test.go
Normal file
57
internal/transport/http/content_disposition_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user