mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
2e14a2a51a
A failed login keeps its username in the in-memory log buffer, the limiter table and the notifier queue, and the route accepted any body the global cap allowed. The login route now takes at most 16 KiB and answers a username over 256 bytes with the generic failure before any of those see it; the user setters refuse such a name too, so no stored username can be one the login route turns away.
95 lines
3.4 KiB
Go
95 lines
3.4 KiB
Go
package controller
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-contrib/sessions/cookie"
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/locale"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/service/panel"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/session"
|
|
)
|
|
|
|
func TestLoginCSRFRejectionLogsPeerIPNotForwardedHeader(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
engine := gin.New()
|
|
engine.Use(sessions.Sessions("3x-ui", cookie.NewStore([]byte("csrf-log-test-secret"))))
|
|
NewIndexController(engine.Group("/"))
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/login", nil)
|
|
req.RemoteAddr = "198.51.100.9:40000"
|
|
req.Header.Set("X-Forwarded-For", "203.0.113.66")
|
|
rec := httptest.NewRecorder()
|
|
engine.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden)
|
|
}
|
|
|
|
var line string
|
|
for _, l := range logger.GetLogs(50, "WARNING") {
|
|
if strings.Contains(l, "CSRF validation failed") {
|
|
line = l
|
|
break
|
|
}
|
|
}
|
|
if !strings.Contains(line, "IP=198.51.100.9") || strings.Contains(line, "203.0.113.66") {
|
|
t.Fatalf("CSRF rejection log = %q, want the untrusted peer 198.51.100.9 and not the forwarded header", line)
|
|
}
|
|
}
|
|
|
|
// A failed login keeps its username in the log buffer, the limiter table and the
|
|
// notifier queue; an unauthenticated caller must not be able to make that unbounded.
|
|
func TestLoginRefusesOversizedUsernameBeforeRecordingIt(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
|
|
engine := gin.New()
|
|
engine.Use(sessions.Sessions("3x-ui", cookie.NewStore([]byte("login-length-test-secret"))))
|
|
engine.Use(func(c *gin.Context) {
|
|
c.Set("I18n", func(_ locale.I18nType, key string, _ ...string) string { return key })
|
|
c.Next()
|
|
})
|
|
NewIndexController(engine.Group("/"))
|
|
|
|
tokenReq := httptest.NewRequest(http.MethodGet, "/csrf-token", nil)
|
|
tokenRec := httptest.NewRecorder()
|
|
engine.ServeHTTP(tokenRec, tokenReq)
|
|
var token struct{ Obj string }
|
|
if err := json.Unmarshal(tokenRec.Body.Bytes(), &token); err != nil || token.Obj == "" {
|
|
t.Fatalf("csrf token: %v %s", err, tokenRec.Body.String())
|
|
}
|
|
username := strings.Repeat("u", panel.MaxUsernameLength+1)
|
|
form := url.Values{"username": {username}, "password": {"wrong"}}
|
|
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
|
req.RemoteAddr = "198.51.100.10:40000"
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set(session.CSRFHeaderName, token.Obj)
|
|
for _, cookie := range tokenRec.Result().Cookies() {
|
|
req.AddCookie(cookie)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
engine.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"success":false`) {
|
|
t.Fatalf("login = %d %s, want the generic failure", rec.Code, rec.Body.String())
|
|
}
|
|
for _, line := range logger.GetLogs(100, "DEBUG") {
|
|
if strings.Contains(line, username) {
|
|
t.Fatal("the oversized username reached the log buffer")
|
|
}
|
|
}
|
|
defaultLoginLimiter.mu.Lock()
|
|
defer defaultLoginLimiter.mu.Unlock()
|
|
if _, kept := defaultLoginLimiter.attempts[loginLimitKey("198.51.100.10", username)]; kept {
|
|
t.Fatal("the oversized username was kept in the limiter table")
|
|
}
|
|
}
|