fix: security & privacy (rate limiter fix, csrf session cookie, logout on server, drafts stale token passw, login timing, password over 72b, JWT_SECRET complexity, SMTP TLS, CSV export neutralised, fonts bundled, upload metadata, hourly cleanup job, privacy.md report)

This commit is contained in:
Florian Sylvain committed 2026-10-22 00:49:19 +02:00
1 parent e2812ee0b9
commit ea98c5ec05
118 files changed
+3828 -130

No files matched your search

@@ -0,0 +1,75 @@
package imaging
import (
"bytes"
"encoding/binary"
)
const (
tiffOrientationTag = 0x0112
tiffTypeShort = 3
tiffHeaderSize = 8
tiffEntrySize = 12
tiffMagic = 42
uprightOrientation = 1
maxOrientation = 8
)
var exifHeader = []byte("Exif\x00\x00")
func exifOrientation(payload []byte) (uint16, bool) {
tiff := bytes.TrimPrefix(payload, exifHeader)
if len(tiff) < tiffHeaderSize {
return 0, false
}
var order binary.ByteOrder
switch string(tiff[:2]) {
case "II":
order = binary.LittleEndian
case "MM":
order = binary.BigEndian
default:
return 0, false
}
if order.Uint16(tiff[2:]) != tiffMagic {
return 0, false
}
ifd := int(order.Uint32(tiff[4:]))
if ifd < tiffHeaderSize || ifd+2 > len(tiff) {
return 0, false
}
count := int(order.Uint16(tiff[ifd:]))
for i := range count {
entry := ifd + 2 + i*tiffEntrySize
if entry+tiffEntrySize > len(tiff) {
return 0, false
}
if order.Uint16(tiff[entry:]) != tiffOrientationTag || order.Uint16(tiff[entry+2:]) != tiffTypeShort {
continue
}
value := order.Uint16(tiff[entry+8:])
if value < uprightOrientation || value > maxOrientation {
return 0, false
}
return value, true
}
return 0, false
}
func needsOrientation(orientation uint16) bool {
return orientation > uprightOrientation
}
func orientationOnlyTIFF(orientation uint16) []byte {
tiff := make([]byte, 0, tiffHeaderSize+2+tiffEntrySize+4)
tiff = append(tiff, 'M', 'M')
tiff = binary.BigEndian.AppendUint16(tiff, tiffMagic)
tiff = binary.BigEndian.AppendUint32(tiff, tiffHeaderSize)
tiff = binary.BigEndian.AppendUint16(tiff, 1)
tiff = binary.BigEndian.AppendUint16(tiff, tiffOrientationTag)
tiff = binary.BigEndian.AppendUint16(tiff, tiffTypeShort)
tiff = binary.BigEndian.AppendUint32(tiff, 1)
tiff = binary.BigEndian.AppendUint16(tiff, orientation)
tiff = binary.BigEndian.AppendUint16(tiff, 0)
return binary.BigEndian.AppendUint32(tiff, 0)
}
@@ -0,0 +1,168 @@
package imaging
import (
"bytes"
"encoding/binary"
)
const (
jpegMarkerPrefix = 0xFF
jpegSOI = 0xD8
jpegEOI = 0xD9
jpegSOS = 0xDA
jpegTEM = 0x01
jpegRST0 = 0xD0
jpegRST7 = 0xD7
jpegAPP0 = 0xE0
jpegAPP1 = 0xE1
jpegAPP2 = 0xE2
jpegAPP14 = 0xEE
jpegAPP15 = 0xEF
jpegCOM = 0xFE
jpegStuffedZero = 0x00
)
var iccProfileSignature = []byte("ICC_PROFILE\x00")
type jpegSegment struct {
marker byte
payload []byte
}
func stripJPEG(data []byte) ([]byte, error) {
if len(data) < 4 || data[0] != jpegMarkerPrefix || data[1] != jpegSOI {
return nil, errMalformedContainer
}
out := []byte{jpegMarkerPrefix, jpegSOI}
insertAt := len(out)
orientation := uint16(uprightOrientation)
first := true
for pos := 2; ; {
marker, next, err := readJPEGMarker(data, pos)
if err != nil {
return nil, err
}
pos = next
if marker == jpegEOI {
out = append(out, jpegMarkerPrefix, jpegEOI)
return withOrientationSegment(out, insertAt, orientation), nil
}
if isStandaloneJPEGMarker(marker) {
out = append(out, jpegMarkerPrefix, marker)
continue
}
segment, next, err := readJPEGSegment(data, pos, marker)
if err != nil {
return nil, err
}
pos = next
if isJPEGMetadata(segment) {
if found, ok := jpegExifOrientation(segment); ok {
orientation = found
}
} else {
out = appendJPEGSegment(out, segment)
if first && segment.marker == jpegAPP0 {
insertAt = len(out)
}
}
first = false
if marker == jpegSOS {
end := jpegScanEnd(data, pos)
out = append(out, data[pos:end]...)
if end == len(data) {
return withOrientationSegment(out, insertAt, orientation), nil
}
pos = end
}
}
}
func readJPEGMarker(data []byte, pos int) (byte, int, error) {
if pos >= len(data) || data[pos] != jpegMarkerPrefix {
return 0, 0, errMalformedContainer
}
for pos < len(data) && data[pos] == jpegMarkerPrefix {
pos++
}
if pos >= len(data) {
return 0, 0, errMalformedContainer
}
return data[pos], pos + 1, nil
}
func readJPEGSegment(data []byte, pos int, marker byte) (jpegSegment, int, error) {
if pos+2 > len(data) {
return jpegSegment{}, 0, errMalformedContainer
}
length := int(binary.BigEndian.Uint16(data[pos:]))
if length < 2 || pos+length > len(data) {
return jpegSegment{}, 0, errMalformedContainer
}
return jpegSegment{marker: marker, payload: data[pos+2 : pos+length]}, pos + length, nil
}
func jpegScanEnd(data []byte, pos int) int {
for pos < len(data) {
if data[pos] != jpegMarkerPrefix || pos+1 >= len(data) {
pos++
continue
}
next := data[pos+1]
if next == jpegStuffedZero || isRestartMarker(next) {
pos += 2
continue
}
return pos
}
return len(data)
}
func isStandaloneJPEGMarker(marker byte) bool {
return marker == jpegTEM || isRestartMarker(marker)
}
func isRestartMarker(marker byte) bool {
return marker >= jpegRST0 && marker <= jpegRST7
}
func isJPEGMetadata(segment jpegSegment) bool {
if segment.marker == jpegCOM {
return true
}
if segment.marker == jpegAPP2 {
return !bytes.HasPrefix(segment.payload, iccProfileSignature)
}
if segment.marker == jpegAPP0 || segment.marker == jpegAPP14 {
return false
}
return segment.marker >= jpegAPP1 && segment.marker <= jpegAPP15
}
func jpegExifOrientation(segment jpegSegment) (uint16, bool) {
if segment.marker != jpegAPP1 || !bytes.HasPrefix(segment.payload, exifHeader) {
return 0, false
}
return exifOrientation(segment.payload)
}
func appendJPEGSegment(out []byte, segment jpegSegment) []byte {
out = append(out, jpegMarkerPrefix, segment.marker)
out = binary.BigEndian.AppendUint16(out, uint16(len(segment.payload)+2))
return append(out, segment.payload...)
}
func withOrientationSegment(out []byte, insertAt int, orientation uint16) []byte {
if !needsOrientation(orientation) {
return out
}
payload := append(bytes.Clone(exifHeader), orientationOnlyTIFF(orientation)...)
segment := appendJPEGSegment(nil, jpegSegment{marker: jpegAPP1, payload: payload})
return append(out[:insertAt:insertAt], append(segment, out[insertAt:]...)...)
}
@@ -0,0 +1,35 @@
package imaging
import (
domain "RenewCMS/internal/domain/image"
"errors"
)
var errMalformedContainer = errors.New("malformed image container")
type MetadataStripper struct{}
var _ domain.MetadataStripper = &MetadataStripper{}
func NewMetadataStripper() *MetadataStripper {
return &MetadataStripper{}
}
func (s *MetadataStripper) Strip(contentType string, data []byte) ([]byte, error) {
var stripped []byte
var err error
switch contentType {
case "image/jpeg":
stripped, err = stripJPEG(data)
case "image/png":
stripped, err = stripPNG(data)
case "image/webp":
stripped, err = stripWebP(data)
default:
return data, nil
}
if err != nil {
return nil, errors.Join(domain.ErrImageUnreadable, err)
}
return stripped, nil
}
@@ -0,0 +1,86 @@
package imaging
import (
"bytes"
"encoding/binary"
"hash/crc32"
)
const (
pngChunkHeaderSize = 8
pngChunkCRCSize = 4
pngIHDR = "IHDR"
pngIEND = "IEND"
pngEXIF = "eXIf"
)
var pngSignature = []byte("\x89PNG\r\n\x1a\n")
var pngMetadataChunks = map[string]bool{
pngEXIF: true,
"tEXt": true,
"zTXt": true,
"iTXt": true,
"tIME": true,
}
func stripPNG(data []byte) ([]byte, error) {
if !bytes.HasPrefix(data, pngSignature) {
return nil, errMalformedContainer
}
out := bytes.Clone(pngSignature)
orientation := uint16(uprightOrientation)
insertAt := 0
for pos := len(pngSignature); pos < len(data); {
available := len(data) - pos - pngChunkHeaderSize - pngChunkCRCSize
if available < 0 {
return nil, errMalformedContainer
}
size := binary.BigEndian.Uint32(data[pos:])
if uint64(size) > uint64(available) {
return nil, errMalformedContainer
}
kind := string(data[pos+4 : pos+pngChunkHeaderSize])
payload := data[pos+pngChunkHeaderSize : pos+pngChunkHeaderSize+int(size)]
end := pos + pngChunkHeaderSize + len(payload) + pngChunkCRCSize
if pngMetadataChunks[kind] {
if kind == pngEXIF {
if found, ok := exifOrientation(payload); ok {
orientation = found
}
}
} else {
out = append(out, data[pos:end]...)
if kind == pngIHDR {
insertAt = len(out)
}
}
pos = end
if kind == pngIEND {
return withPNGOrientation(out, insertAt, orientation)
}
}
return nil, errMalformedContainer
}
func withPNGOrientation(out []byte, insertAt int, orientation uint16) ([]byte, error) {
if !needsOrientation(orientation) {
return out, nil
}
if insertAt == 0 {
return nil, errMalformedContainer
}
chunk := pngChunk(pngEXIF, orientationOnlyTIFF(orientation))
return append(out[:insertAt:insertAt], append(chunk, out[insertAt:]...)...), nil
}
func pngChunk(kind string, payload []byte) []byte {
chunk := binary.BigEndian.AppendUint32(nil, uint32(len(payload)))
chunk = append(chunk, kind...)
chunk = append(chunk, payload...)
return binary.BigEndian.AppendUint32(chunk, crc32.ChecksumIEEE(chunk[4:]))
}
@@ -0,0 +1,88 @@
package imaging
import (
"bytes"
"encoding/binary"
)
const (
riffHeaderSize = 12
riffChunkHeaderSize = 8
webpVP8X = "VP8X"
webpEXIF = "EXIF"
webpXMP = "XMP "
webpEXIFFlag = 0x08
webpXMPFlag = 0x04
)
func stripWebP(data []byte) ([]byte, error) {
if len(data) < riffHeaderSize || string(data[:4]) != "RIFF" || string(data[8:12]) != "WEBP" {
return nil, errMalformedContainer
}
declared := int(binary.LittleEndian.Uint32(data[4:])) + riffChunkHeaderSize
if declared > len(data) || declared < riffHeaderSize {
return nil, errMalformedContainer
}
data = data[:declared]
var chunks [][]byte
orientation := uint16(uprightOrientation)
for pos := riffHeaderSize; pos < len(data); {
available := len(data) - pos - riffChunkHeaderSize
if available < 0 {
return nil, errMalformedContainer
}
size := binary.LittleEndian.Uint32(data[pos+4:])
padded := uint64(size) + uint64(size%2)
if padded > uint64(available) {
return nil, errMalformedContainer
}
kind := string(data[pos : pos+4])
payload := data[pos+riffChunkHeaderSize : pos+riffChunkHeaderSize+int(size)]
end := pos + riffChunkHeaderSize + int(padded)
switch kind {
case webpEXIF:
if found, ok := exifOrientation(payload); ok {
orientation = found
}
case webpXMP:
default:
chunks = append(chunks, bytes.Clone(data[pos:end]))
}
pos = end
}
if len(chunks) == 0 {
return nil, errMalformedContainer
}
if string(chunks[0][:4]) == webpVP8X {
if len(chunks[0]) <= riffChunkHeaderSize {
return nil, errMalformedContainer
}
flags := &chunks[0][riffChunkHeaderSize]
*flags &^= webpEXIFFlag | webpXMPFlag
if needsOrientation(orientation) {
*flags |= webpEXIFFlag
chunks = append(chunks, riffChunk(webpEXIF, orientationOnlyTIFF(orientation)))
}
}
return riffContainer(chunks), nil
}
func riffChunk(kind string, payload []byte) []byte {
chunk := append([]byte(kind), binary.LittleEndian.AppendUint32(nil, uint32(len(payload)))...)
chunk = append(chunk, payload...)
if len(payload)%2 == 1 {
chunk = append(chunk, 0)
}
return chunk
}
func riffContainer(chunks [][]byte) []byte {
body := []byte("WEBP")
for _, chunk := range chunks {
body = append(body, chunk...)
}
out := append([]byte("RIFF"), binary.LittleEndian.AppendUint32(nil, uint32(len(body)))...)
return append(out, body...)
}