Sicherheit & Robustheit härten

- SQLite-DSN: busy_timeout(5000) + WAL + foreign_keys gegen SQLITE_BUSY
- uid ist nie 0 mehr (0 = Sentinel für gelöscht/anonym), Retry bei Kollision
- scanEntries gibt Scan-Fehler zurück statt Zeilen still zu überspringen
- Session-timeout serverseitig auf 30 Tage gedeckelt (clientgesteuert)
- Bild-Upload: Dimensionen vor Decode prüfen (Decompression-Bomb-Schutz)
- Secure-Cookie via TLS-Erkennung (r.TLS / X-Forwarded-Proto)
- Rate-Limit (httprate, 20/min/IP) auf Login & Registrierung
- Vote nur auf existierende Beiträge (keine Waisen-Votes)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 17:07:17 +02:00
parent 5478e92253
commit 1313e9613c
7 changed files with 95 additions and 20 deletions
+23 -1
View File
@@ -7,6 +7,7 @@ import (
"image/gif"
"image/jpeg"
_ "image/png"
"io"
"math/rand/v2"
"mime/multipart"
"net/http"
@@ -48,7 +49,7 @@ func scanEntries(rows *sql.Rows) ([]Entry, error) {
var e Entry
if err := rows.Scan(&e.PID, &e.UID, &e.CreatedAt, &e.Content, &e.Filepath,
&e.ReplyTo, &e.ReplyCount, &e.LastActivity, &e.Deleted, &e.Username); err != nil {
continue
return nil, err
}
entries = append(entries, e)
}
@@ -393,6 +394,12 @@ func handleVote(w http.ResponseWriter, r *http.Request) {
return
}
// Nur auf existierende Beiträge abstimmen, sonst entstehen Waisen-Votes.
if _, err := entryByPID(pid); err != nil {
writeError(w, http.StatusNotFound, "Beitrag nicht gefunden")
return
}
var prev string
readErr := db.QueryRow(`SELECT mode FROM vote WHERE uid = ? AND pid = ?`, uid, pid).Scan(&prev)
if readErr != nil && readErr != sql.ErrNoRows {
@@ -428,6 +435,21 @@ func storeImage(fh *multipart.FileHeader) (string, error) {
}
defer src.Close()
// Dimensionen aus dem Header prüfen, bevor wir die vollen Pixel allokieren:
// eine kleine Datei kann riesige Maße deklarieren (Decompression-Bomb) und
// sonst beim Decode den Speicher sprengen.
const maxPixels = 50_000_000 // ~50 MP
cfg, _, err := image.DecodeConfig(src)
if err != nil {
return "", err
}
if cfg.Width <= 0 || cfg.Height <= 0 || cfg.Width*cfg.Height > maxPixels {
return "", fmt.Errorf("bild zu groß: %dx%d", cfg.Width, cfg.Height)
}
if _, err := src.Seek(0, io.SeekStart); err != nil {
return "", err
}
img, format, err := image.Decode(src)
if err != nil {
return "", err