forked from alterminal/alterminal
first commit
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user