From 12eaaf186fce84d069556e11fea85e0be42c1a8b Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Mon, 16 Feb 2026 08:00:53 -0800 Subject: Fix restricted login access and modernize login page - Close NK-oqd24q: Fix login access for v3/api - Update web.go to exclude /login/ from CSRF check during initial submission - Modernize web/static/login.html with new CSS and structure - Add web/login_test.go to verify CSRF exclusion - Created NK-ngokc3 for further CSRF enhancements --- web/login_test.go | 68 +++++++++++++++++++++++ web/static/login.html | 151 +++++++++++++++++++++++++++++++++++++++++++------- web/web.go | 2 +- 3 files changed, 200 insertions(+), 21 deletions(-) create mode 100644 web/login_test.go (limited to 'web') diff --git a/web/login_test.go b/web/login_test.go new file mode 100644 index 0000000..b48e7bc --- /dev/null +++ b/web/login_test.go @@ -0,0 +1,68 @@ +package web + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "golang.org/x/crypto/bcrypt" + + "adammathes.com/neko/config" +) + +func TestCSRFLoginExclusion(t *testing.T) { + // Setup password + pw := "secret" + hashed, _ := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) + // We need to set the global config since loginHandler uses it + config.Config.DigestPassword = string(hashed) // wait, config stores the hashed password? + // let's check web.go loginHandler: + // if password == config.Config.DigestPassword { + // v, _ := bcrypt.GenerateFromPassword([]byte(password), 0) + + // Ah, it compares PLAIN TEXT password with config.Config.DigestPassword. + // wait, line 113: if password == config.Config.DigestPassword { + // So config stores the PLAIN TEXT password? That's... odd, but okay for this test. + originalPw := config.Config.DigestPassword + defer func() { config.Config.DigestPassword = originalPw }() + config.Config.DigestPassword = pw + + // Create a mux with login handler + mux := http.NewServeMux() + mux.HandleFunc("/login/", loginHandler) + mux.HandleFunc("/other", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + // Wrap with CSRF middleware + // We need to pass a pointer to Settings + handler := CSRFMiddleware(&config.Config, mux) + + // Test 1: POST /login/ without CSRF token + // Should NOT return 403 Forbidden. + // Since we provide correct password, it should redirect (307) + req := httptest.NewRequest("POST", "/login/", strings.NewReader("password="+pw)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + + if rr.Code == http.StatusForbidden { + t.Errorf("Expected /login/ POST to be allowed without CSRF token, got 403 Forbidden") + } + if rr.Code != http.StatusTemporaryRedirect { + t.Errorf("Expected 307 Redirect on successful login, got %d", rr.Code) + } + + // Test 2: POST /other without CSRF token + // Should fail with 403 Forbidden + req2 := httptest.NewRequest("POST", "/other", nil) + rr2 := httptest.NewRecorder() + + handler.ServeHTTP(rr2, req2) + + if rr2.Code != http.StatusForbidden { + t.Errorf("Expected 403 Forbidden for POST /other, got %d", rr2.Code) + } +} diff --git a/web/static/login.html b/web/static/login.html index ab3e781..c7d0a03 100644 --- a/web/static/login.html +++ b/web/static/login.html @@ -1,23 +1,134 @@ - - - neko rss mode - - - - - - -
-

username

- - -

password

- - - -
- - + + + + + Neko Reader Login + + + +
+

Welcome Back

+
+
+ + +
+
+ + +
+ +
+ +
+ diff --git a/web/web.go b/web/web.go index a287bb9..d59d308 100644 --- a/web/web.go +++ b/web/web.go @@ -379,7 +379,7 @@ func CSRFMiddleware(cfg *config.Settings, next http.Handler) http.Handler { token = cookie.Value } - if (r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodDelete) && r.URL.Path != "/api/login" { + if (r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodDelete) && r.URL.Path != "/api/login" && r.URL.Path != "/login/" { headerToken := r.Header.Get("X-CSRF-Token") if headerToken == "" || headerToken != token { http.Error(w, "CSRF token mismatch", http.StatusForbidden) -- cgit v1.2.3