120 lines
4.1 KiB
Go
120 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|