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

+105
View File
@@ -0,0 +1,105 @@
package test
import (
"RenewCMS/api/middleware"
"RenewCMS/api/pkg/apperr"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func newCrossOriginProtectedHandler(publicURL string) (http.Handler, *int) {
reached := 0
handler := middleware.CrossOriginProtection(publicURL)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
reached++
w.WriteHeader(http.StatusOK)
}))
return handler, &reached
}
func sendCrossOriginRequest(handler http.Handler, method string, headers map[string]string) *httptest.ResponseRecorder {
req := httptest.NewRequest(method, "https://cms.example.com/v1/user/invite", strings.NewReader(`{"email":"attacker@evil.example"}`))
req.Header.Set("Content-Type", "text/plain")
req.AddCookie(&http.Cookie{Name: "jwt", Value: "session"})
for name, value := range headers {
req.Header.Set(name, value)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
return rec
}
func TestCrossOriginProtection(t *testing.T) {
refused := []struct {
name string
method string
headers map[string]string
}{
{"a cross-site form POST", http.MethodPost, map[string]string{"Sec-Fetch-Site": "cross-site", "Origin": "https://evil.example"}},
{"a same-site but cross-origin POST from a sibling subdomain", http.MethodPost, map[string]string{"Sec-Fetch-Site": "same-site", "Origin": "https://blog.example.com"}},
{"a cross-site DELETE", http.MethodDelete, map[string]string{"Sec-Fetch-Site": "cross-site", "Origin": "https://evil.example"}},
{"an old browser's cross-origin POST without Sec-Fetch-Site", http.MethodPost, map[string]string{"Origin": "https://evil.example"}},
}
for _, tc := range refused {
t.Run("Refuses "+tc.name, func(t *testing.T) {
handler, reached := newCrossOriginProtectedHandler("https://cms.example.com")
rec := sendCrossOriginRequest(handler, tc.method, tc.headers)
if rec.Code != http.StatusForbidden {
t.Errorf("expected 403, got %d", rec.Code)
}
if !strings.Contains(rec.Body.String(), apperr.CrossOriginErrorMessage) {
t.Errorf("expected the cross-origin message, got %q", rec.Body.String())
}
if *reached != 0 {
t.Error("the handler must not run")
}
})
}
allowed := []struct {
name string
method string
headers map[string]string
}{
{"a same-origin POST from the SPA", http.MethodPost, map[string]string{"Sec-Fetch-Site": "same-origin", "Origin": "https://cms.example.com"}},
{"a request typed in the address bar", http.MethodPost, map[string]string{"Sec-Fetch-Site": "none"}},
{"a non-browser client with neither header", http.MethodPost, nil},
{"a cross-site GET", http.MethodGet, map[string]string{"Sec-Fetch-Site": "cross-site", "Origin": "https://evil.example"}},
{"a cross-site preflight", http.MethodOptions, map[string]string{"Sec-Fetch-Site": "cross-site", "Origin": "https://evil.example"}},
{"a cross-origin POST carrying an explicit Authorization header", http.MethodPost, map[string]string{"Sec-Fetch-Site": "cross-site", "Origin": "https://shop.example", "Authorization": "Bearer rcms_key"}},
{"an old browser's POST from the public URL behind a proxy that rewrites Host", http.MethodPost, map[string]string{"Origin": "https://cms.example.com", "Host": "127.0.0.1:8080"}},
}
for _, tc := range allowed {
t.Run("Allows "+tc.name, func(t *testing.T) {
handler, reached := newCrossOriginProtectedHandler("https://cms.example.com")
req := httptest.NewRequest(tc.method, "https://cms.example.com/v1/user/invite", nil)
for name, value := range tc.headers {
if name == "Host" {
req.Host = value
continue
}
req.Header.Set(name, value)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK || *reached != 1 {
t.Errorf("expected the handler to run, got %d (reached %d)", rec.Code, *reached)
}
})
}
t.Run("An unparsable public URL still protects against cross-site requests", func(t *testing.T) {
handler, reached := newCrossOriginProtectedHandler("::not a url")
rec := sendCrossOriginRequest(handler, http.MethodPost, map[string]string{"Sec-Fetch-Site": "cross-site", "Origin": "https://evil.example"})
if rec.Code != http.StatusForbidden || *reached != 0 {
t.Errorf("expected 403 without reaching the handler, got %d (reached %d)", rec.Code, *reached)
}
})
}