Files
gis/internal/transport/http/content_disposition.go
Bakhtiyar Issakhmetov 5b44cbda2f 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>
2026-10-15 21:44:10 +05:00

64 lines
1.7 KiB
Go

package http
import (
"fmt"
"strings"
)
// fallbackDownloadName is used when nothing printable survives sanitisation.
const fallbackDownloadName = "download"
// contentDisposition builds a Content-Disposition header value for a stored,
// user-supplied filename following RFC 6266: an ASCII-only quoted fallback in
// filename= and the exact UTF-8 name, percent-encoded per RFC 8187, in
// filename*=. Neither parameter can carry quotes, backslashes, control
// characters (CR/LF) or parameter separators taken from the filename, so a
// crafted upload name cannot inject extra header parameters or headers.
func contentDisposition(disposition, filename string) string {
name := strings.Map(func(r rune) rune {
if r < 0x20 || r == 0x7f {
return -1 // drop control characters, including CR and LF
}
return r
}, filename)
name = strings.TrimSpace(name)
if name == "" {
name = fallbackDownloadName
}
ascii := strings.Map(func(r rune) rune {
if r > 0x7e || r == '"' || r == '\\' {
return '_'
}
return r
}, name)
return fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, ascii, encodeRFC8187(name))
}
// encodeRFC8187 percent-encodes every byte of s that is not an attr-char
// (RFC 8187 §3.2.1).
func encodeRFC8187(s string) string {
const hex = "0123456789ABCDEF"
var b strings.Builder
for i := 0; i < len(s); i++ {
c := s[i]
if isAttrChar(c) {
b.WriteByte(c)
continue
}
b.WriteByte('%')
b.WriteByte(hex[c>>4])
b.WriteByte(hex[c&0x0f])
}
return b.String()
}
func isAttrChar(c byte) bool {
switch {
case 'a' <= c && c <= 'z', 'A' <= c && c <= 'Z', '0' <= c && c <= '9':
return true
}
return strings.IndexByte("!#$&+-.^_`|~", c) >= 0
}