a73x

internal/server/api/authreject_test.go

Ref:   Size: 2.7 KiB   History

package api

import (
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

// TestSanitizeOriginPassesOnlyPlainHTTPOrigins pins every clause of the guard
// set, one row per independently-removable clause.
//
// What the guards protect is not HTML injection — the value is emitted through
// http.Error (text/plain, nosniff) and slog. It is the phishing-copy surface
// and log hygiene: the wrong_origin page prints "it started at <this string>"
// to whoever followed a crafted callback link, and the log line carries it as
// got_host=. The length cap bounds what one request can push into both. So the
// clauses are not paranoia and must not be simplified away.
func TestSanitizeOriginPassesOnlyPlainHTTPOrigins(t *testing.T) {
	// 7 for "http://" + 193 of host = exactly the 200-char cap.
	atCap := "http://" + strings.Repeat("a", 189) + ".com"
	overCap := "http://" + strings.Repeat("a", 190) + ".com"

	for _, tc := range []struct {
		guard string // the clause this row fails if removed
		in    string
		want  string
	}{
		{"length cap", overCap, ""},
		{"length cap boundary", atCap, atCap},

		{"scheme allowlist", "javascript:alert(1)", ""},
		{"scheme allowlist", "data:text/html,x", ""},
		{"scheme allowlist", "ftp://h", ""},
		{"scheme allowlist", "evil.com", ""}, // scheme-less: url.Parse reads it as a path
		{"scheme allowlist", "http://h", "http://h"},
		{"scheme allowlist", "https://h", "https://h"},

		{"non-empty host", "http://", ""},

		{"no path", "http://h/path", ""},
		{"no query", "http://h?q=1", ""},
		{"no fragment", "http://h#f", ""},
		{"no userinfo", "http://u:p@h", ""},

		{"host charset", "http://h<x>", ""},
		{"host charset", "http://h%41", ""}, // percent-encoding hides anything
		{"host charset", "http://h x", ""},
		{"host charset", "http://hé", ""},
		{"host charset: IPv6 and port stay legal", "http://[::1]:8080", "http://[::1]:8080"},
		{"host charset: hyphen and dot stay legal", "http://a-b.c:9090", "http://a-b.c:9090"},

		// The echoed string is re-emitted from the parsed parts, so the scheme
		// comes back lowercased and the host does not. Pinned so a rewrite
		// cannot quietly change what the error page shows.
		{"re-emission is scheme-normalized, host-verbatim", "HTTP://H", "http://H"},
	} {
		t.Run(tc.guard+" "+tc.in, func(t *testing.T) {
			got := sanitizeOrigin(tc.in)
			if tc.want == "" {
				assert.Empty(t, got,
					"the %s guard must reject %q: it reached an error page a victim reads and a log line, and nothing downstream re-checks it", tc.guard, tc.in)
				return
			}
			assert.Equal(t, tc.want, got,
				"the %s guard must pass %q unchanged: rejecting it costs a real sign-in its wrong-origin diagnosis", tc.guard, tc.in)
		})
	}
}