package oidc import ( "context" "net/http" "net/http/httptest" "net/url" "testing" "golang.org/x/oauth2" "aether/internal/avatar" ) // Die Bildadresse stammt aus einem Token, also von außen. Erlaubt ist genau // der Ursprung des Ausstellers – alles andere wäre eine Aufforderung an den // Server, für einen Fremden beliebige Adressen anzufragen. func TestAllowedHost(t *testing.T) { h := &Handler{cfg: Config{Issuer: "https://id.example.com"}} erlaubt := []string{ "https://id.example.com/api/users/1/profile-picture.png", "https://ID.EXAMPLE.COM/bild.png", // Groß-/Kleinschreibung zählt nicht "https://id.example.com:443/bild.png", } for _, raw := range erlaubt { u, err := url.Parse(raw) if err != nil { t.Fatal(err) } if err := h.allowedHost(u); err != nil { t.Errorf("%s abgelehnt: %v", raw, err) } } verboten := []string{ "http://169.254.169.254/latest/meta-data/", // Metadaten der Umgebung "http://localhost:8080/api/v1/admin/users", // die eigene App "http://postgres:5432/", // Nachbarcontainer "https://id.example.com.angreifer.net/x.png", // Name nur vorne gleich "https://id.example.com:9200/x.png", // gleicher Rechner, anderer Dienst "file:///etc/passwd", "gopher://id.example.com/", } for _, raw := range verboten { u, err := url.Parse(raw) if err != nil { t.Fatal(err) } if err := h.allowedHost(u); err == nil { t.Errorf("%s wurde durchgelassen", raw) } } } // Ein Anbieter, der selbst auf einem Port läuft, muss zu sich selbst passen. func TestAllowedHostMitPortImIssuer(t *testing.T) { h := &Handler{cfg: Config{Issuer: "http://id.example.com:9000"}} ok, _ := url.Parse("http://id.example.com:9000/bild.png") if err := h.allowedHost(ok); err != nil { t.Errorf("eigener Port abgelehnt: %v", err) } nein, _ := url.Parse("http://id.example.com/bild.png") if err := h.allowedHost(nein); err == nil { t.Error("Port 80 wurde für einen Anbieter auf 9000 durchgelassen") } } // Der ganze Weg über das Netz – mit einem Anbieter, der erst 401 sagt und das // Bild nur gegen Token herausgibt. Genau so verhalten sich Anbieter, die // Profilbilder nicht öffentlich stellen. func TestFetchAvatarHoltMitTokenNach(t *testing.T) { var ohneToken, mitToken int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Bearer geheim" { ohneToken++ w.WriteHeader(http.StatusUnauthorized) return } mitToken++ w.Write([]byte("PNG-Attrappe")) })) defer srv.Close() h := &Handler{cfg: Config{Issuer: srv.URL}} body, err := h.fetchAvatar(context.Background(), srv.URL+"/bild.png", &oauth2.Token{AccessToken: "geheim"}) if err != nil { t.Fatalf("fetchAvatar: %v", err) } if string(body) != "PNG-Attrappe" { t.Errorf("Inhalt %q", body) } if ohneToken != 1 || mitToken != 1 { t.Errorf("erwartet je ein Versuch, gemessen ohne=%d mit=%d", ohneToken, mitToken) } } // Eine Weiterleitung darf den Anbieter nicht verlassen – sonst wäre die // Prüfung der ersten Adresse wertlos. func TestFetchAvatarFolgtKeinerFremdenWeiterleitung(t *testing.T) { fremd := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Error("der fremde Server wurde angefragt") })) defer fremd.Close() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, fremd.URL+"/beute", http.StatusFound) })) defer srv.Close() h := &Handler{cfg: Config{Issuer: srv.URL}} if _, err := h.fetchAvatar(context.Background(), srv.URL+"/bild.png", nil); err == nil { t.Fatal("Weiterleitung auf einen fremden Rechner wurde befolgt") } } // Was der Anbieter schickt, ist unbegrenzt groß denkbar. Mehr als MaxBytes // darf nie in den Speicher wandern. func TestFetchAvatarBegrenztDieGroesse(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { buf := make([]byte, 1<<20) for i := 0; i < 8; i++ { if _, err := w.Write(buf); err != nil { return } } })) defer srv.Close() h := &Handler{cfg: Config{Issuer: srv.URL}} body, err := h.fetchAvatar(context.Background(), srv.URL+"/riesig.png", nil) if err != nil { t.Fatalf("fetchAvatar: %v", err) } if len(body) > avatar.MaxBytes { t.Errorf("%d Bytes gelesen, erlaubt sind %d", len(body), avatar.MaxBytes) } }