- 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>
64 lines
1.7 KiB
Go
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
|
|
}
|