forked from alterminal/alterminal
109 lines
3.2 KiB
Go
109 lines
3.2 KiB
Go
package auth
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// SafeNext 僅接受站內路徑,阻擋外站與協定相對 URL(open redirect)。
|
|
func TestSafeNext(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{"站內路徑", "/authorize?client_id=x", "/authorize?client_id=x"},
|
|
{"未提供", "", "/"},
|
|
{"外站絕對 URL", "https://evil.example/phish", "/"},
|
|
{"協定相對 URL", "//evil.example", "/"},
|
|
{"相對路徑", "admin/keys", "/"},
|
|
{"僅 scheme", "javascript:alert(1)", "/"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := SafeNext(tt.in); got != tt.want {
|
|
t.Fatalf("SafeNext(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// GET /login?next=... 應在表單保留 next;已登入時導向 next 而非 /。
|
|
func TestLoginPageNext(t *testing.T) {
|
|
next := "/authorize%3Fclient_id%3Dabc" // 已編碼的 query 值
|
|
|
|
t.Run("表單含隱藏 next 欄位", func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
LoginPageHandler(nil)(rec, httptest.NewRequest(http.MethodGet, "/login?next="+next, nil))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), `name="next"`) {
|
|
t.Fatalf("登入表單應保留 next 隱藏欄位: %s", rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("未帶 next 時不出現隱藏欄位", func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
LoginPageHandler(nil)(rec, httptest.NewRequest(http.MethodGet, "/login", nil))
|
|
if strings.Contains(rec.Body.String(), `name="next"`) {
|
|
t.Fatal("無 next 時不需要隱藏欄位")
|
|
}
|
|
})
|
|
}
|
|
|
|
// 表單登入成功後導向 next;惡意的 next 一律回到 /。
|
|
func TestLoginHandlerFormNextRedirect(t *testing.T) {
|
|
db, err := openTestDB()
|
|
if err != nil {
|
|
t.Skipf("資料庫不可用,略過整合測試: %v", err)
|
|
}
|
|
suffix, err := NewToken(6)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
u := &User{Username: "next-" + suffix, Email: "next-" + suffix + "@example.com"}
|
|
if err := u.SetPassword("sup3r-secret"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(u).Error; err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
db.Delete(&Session{}, "user_id = ?", u.ID)
|
|
db.Delete(&User{}, u.ID)
|
|
})
|
|
|
|
login := func(next string) *httptest.ResponseRecorder {
|
|
body := "csrf_token=token-A&username=" + u.Username + "&password=sup3r-secret"
|
|
if next != "" {
|
|
body += "&next=" + next
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
LoginHandler(db)(rec, formPost(body, &http.Cookie{Name: CSRFCookieName, Value: "token-A"}))
|
|
return rec
|
|
}
|
|
|
|
t.Run("合法 next 導向原路徑", func(t *testing.T) {
|
|
rec := login("%2Fauthorize%3Fclient_id%3Dabc")
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
|
|
}
|
|
if loc := rec.Header().Get("Location"); loc != "/authorize?client_id=abc" {
|
|
t.Fatalf("Location = %q, want /authorize?client_id=abc", loc)
|
|
}
|
|
})
|
|
|
|
t.Run("惡意 next 導向首頁", func(t *testing.T) {
|
|
rec := login("https%3A%2F%2Fevil.example")
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("status = %d", rec.Code)
|
|
}
|
|
if loc := rec.Header().Get("Location"); loc != "/" {
|
|
t.Fatalf("Location = %q, want /", loc)
|
|
}
|
|
})
|
|
}
|