Umbau-Projekt (SSR und Rest)

This commit is contained in:
2026-09-03 22:20:55 +02:00
parent 13363cb568
commit c66df283d1
37 changed files with 3719 additions and 344 deletions
+119
View File
@@ -0,0 +1,119 @@
package main
import (
"net/http/httptest"
"strings"
"testing"
)
// TestSafeReturn deckt die Open-Redirect-Abwehr ab. Jede hier durchgelassene
// Eingabe landet später in einem Location-Header -- die abgelehnten Fälle sind
// deshalb wichtiger als die erlaubten.
func TestSafeReturn(t *testing.T) {
const fallback = "/"
erlaubt := []string{
"/",
"/u/alice",
"/u/alice?page=3",
"/?page=2#e123",
"/u/alice?q=%22katzen%22&page=1#e42",
"/e/123",
}
for _, v := range erlaubt {
if got := safeReturn(v, fallback); got != v {
t.Errorf("safeReturn(%q) = %q, erwartet unverändert", v, got)
}
}
abgelehnt := map[string]string{
"leer": "",
"protokoll-relativ": "//evil.example/x",
"drei slashes": "///evil.example",
"backslash-variante": `/\evil.example`,
"absolute https": "https://evil.example",
"absolute http": "http://evil.example",
"javascript-schema": "javascript:alert(1)",
"data-schema": "data:text/html,<script>",
"relativ ohne slash": "u/alice",
"header-injection CR": "/x\r\nSet-Cookie: a=b",
"header-injection LF": "/x\nSet-Cookie: a=b",
"nullbyte": "/x\x00y",
"zu lang": "/" + strings.Repeat("a", 512),
"backslash am anfang": `\/evil.example`,
"leerzeichen davor": " /u/alice",
}
for name, v := range abgelehnt {
if got := safeReturn(v, fallback); got != fallback {
t.Errorf("%s: safeReturn(%q) = %q, erwartet Fallback %q", name, v, got, fallback)
}
}
// Der Fallback wird durchgereicht, nicht hartkodiert.
if got := safeReturn("//evil.example", "/u/bob"); got != "/u/bob" {
t.Errorf("eigener Fallback: bekam %q", got)
}
}
// TestReturnTo prüft die beiden Quellen: bei GET die aufgerufene URL (damit
// Filter und Seitenzahl automatisch mitreisen), bei POST das mitgeschickte
// Feld (damit ein neu gerendertes Formular sein Ziel behält).
func TestReturnTo(t *testing.T) {
get := httptest.NewRequest("GET", "/u/alice?page=3&q=katzen", nil)
if got, want := returnTo(get), "/u/alice?page=3&q=katzen"; got != want {
t.Errorf("GET: returnTo = %q, erwartet %q", got, want)
}
// Eine bösartige URL kann per GET gar nicht erst entstehen (der Pfad kommt
// vom Router), aber die Prüfung muss trotzdem greifen.
odd := httptest.NewRequest("GET", "http://example.com//evil.example", nil)
if got := returnTo(odd); got != "/" {
t.Errorf("GET mit // : returnTo = %q, erwartet /", got)
}
post := httptest.NewRequest("POST", "/entry/1/vote",
strings.NewReader("return_to=%2Fu%2Falice%3Fpage%3D3%23e42&mode=left"))
post.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if got, want := returnTo(post), "/u/alice?page=3#e42"; got != want {
t.Errorf("POST: returnTo = %q, erwartet %q", got, want)
}
evil := httptest.NewRequest("POST", "/entry/1/vote",
strings.NewReader("return_to=https%3A%2F%2Fevil.example"))
evil.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if got := returnTo(evil); got != "/" {
t.Errorf("POST mit fremder URL: returnTo = %q, erwartet /", got)
}
// Ohne Feld: Fallback, nicht die POST-Ziel-URL.
bare := httptest.NewRequest("POST", "/entry/1/vote", nil)
if got := returnTo(bare); got != "/" {
t.Errorf("POST ohne return_to: returnTo = %q, erwartet /", got)
}
}
// TestRedirectBack stellt sicher, dass die Umleitung nach einer Aktion ein 303
// ist (GET auf das Ziel, Reload wiederholt die Aktion nicht) und dass ein
// manipuliertes return_to nicht im Location-Header landet.
func TestRedirectBack(t *testing.T) {
post := func(body string) *httptest.ResponseRecorder {
r := httptest.NewRequest("POST", "/entry/1/vote", strings.NewReader(body))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
redirectBack(w, r, "/")
return w
}
w := post("return_to=%2Fu%2Falice%23e42")
if w.Code != 303 {
t.Errorf("Status = %d, erwartet 303", w.Code)
}
if got, want := w.Header().Get("Location"), "/u/alice#e42"; got != want {
t.Errorf("Location = %q, erwartet %q", got, want)
}
w = post("return_to=%2F%2Fevil.example")
if got := w.Header().Get("Location"); got != "/" {
t.Errorf("manipuliertes Ziel: Location = %q, erwartet /", got)
}
}