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:
@@ -18,9 +18,31 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// maxUploadBytes caps the in-memory portion of a multipart upload (64 MiB).
|
||||
// maxUploadBytes caps the in-memory portion of a multipart upload (64 MiB);
|
||||
// larger parts spill to temporary files. The total request size is capped by
|
||||
// the limitRequestBody middleware (HTTP_MAX_UPLOAD_BYTES).
|
||||
const maxUploadBytes = 64 << 20
|
||||
|
||||
// parseMultipartForm parses a multipart upload and writes the error response
|
||||
// itself when that fails: 413 when the body exceeds the router's size cap, 400
|
||||
// otherwise. It reports whether the handler may continue.
|
||||
func parseMultipartForm(w http.ResponseWriter, r *http.Request) bool {
|
||||
// #nosec G120 -- r.Body is wrapped in http.MaxBytesReader by the
|
||||
// limitRequestBody middleware that NewRouter installs on every route.
|
||||
err := r.ParseMultipartForm(maxUploadBytes)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
var tooLarge *http.MaxBytesError
|
||||
if errors.As(err, &tooLarge) {
|
||||
httputil.WriteError(w, http.StatusRequestEntityTooLarge,
|
||||
"request body exceeds the "+strconv.FormatInt(tooLarge.Limit, 10)+" byte limit")
|
||||
return false
|
||||
}
|
||||
httputil.WriteError(w, http.StatusBadRequest, "request must be multipart/form-data")
|
||||
return false
|
||||
}
|
||||
|
||||
// DatasetHandler serves the /datasets routes.
|
||||
type DatasetHandler struct {
|
||||
svc *service.DatasetService
|
||||
@@ -121,8 +143,7 @@ func (h *DatasetHandler) mapping(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *DatasetHandler) upload(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseMultipartForm(maxUploadBytes); err != nil {
|
||||
httputil.WriteError(w, http.StatusBadRequest, "request must be multipart/form-data")
|
||||
if !parseMultipartForm(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -583,7 +604,7 @@ func (h *DatasetHandler) geojson(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/geo+json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(fc)
|
||||
_, _ = w.Write(fc) // #nosec G705 -- JSON body served as application/geo+json with nosniff, never as HTML
|
||||
}
|
||||
|
||||
// cog streams the raster dataset's Cloud-Optimized GeoTIFF. It responds 422 for
|
||||
@@ -608,7 +629,7 @@ func (h *DatasetHandler) cog(w http.ResponseWriter, r *http.Request) {
|
||||
defer obj.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "image/tiff")
|
||||
w.Header().Set("Content-Disposition", `inline; filename="`+cogFilename(dataset.Filename)+`"`)
|
||||
w.Header().Set("Content-Disposition", contentDisposition("inline", cogFilename(dataset.Filename)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := io.Copy(w, obj); err != nil {
|
||||
// Headers are already sent; nothing useful to return to the client.
|
||||
@@ -637,7 +658,7 @@ func (h *DatasetHandler) download(w http.ResponseWriter, r *http.Request) {
|
||||
defer obj.Close()
|
||||
|
||||
w.Header().Set("Content-Type", dataset.ContentType)
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+dataset.Filename+`"`)
|
||||
w.Header().Set("Content-Disposition", contentDisposition("attachment", dataset.Filename))
|
||||
if dataset.SizeBytes > 0 {
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(dataset.SizeBytes, 10))
|
||||
}
|
||||
@@ -655,8 +676,7 @@ func (h *DatasetHandler) uploadSLD(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := r.ParseMultipartForm(maxUploadBytes); err != nil {
|
||||
httputil.WriteError(w, http.StatusBadRequest, "request must be multipart/form-data")
|
||||
if !parseMultipartForm(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -708,7 +728,7 @@ func (h *DatasetHandler) downloadSLD(w http.ResponseWriter, r *http.Request) {
|
||||
filename = *dataset.SldFilename
|
||||
}
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
||||
w.Header().Set("Content-Disposition", contentDisposition("attachment", filename))
|
||||
if _, err := io.Copy(w, obj); err != nil {
|
||||
// Headers are already sent; nothing useful to return to the client.
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user