Forráskód Böngészése

fix(security): revoke other sessions when the password changes

Changing a password left every existing web session signed in. Since
sessions last 30 days from login by default, someone who had already
signed in with a leaked password kept access after the owner changed
it. Such a session can read the credentials saved for integrations,
create API keys and register passkeys.

Revoke the user's web sessions when their password changes:

- On the authentication page, the current session stays signed in,
  but gets a new ID and secret so that a copied cookie stops working.
- When an administrator changes a password on the user edit page, all
  of that user's sessions are revoked. Administrators editing their
  own account keep their current session.
- Through the API and the -reset-password command, all of the user's
  sessions are revoked.

API keys, passkeys and the Fever and Google Reader credentials are
managed separately and are left untouched. Basic authentication on the
API already checks the password on every request.
Fred 10 órája
szülő
commit
229035a4df

+ 7 - 0
internal/api/user_handlers.go

@@ -103,6 +103,13 @@ func (h *handler) updateUserHandler(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	if userModificationRequest.Password != nil {
+		if err := h.store.RemoveUserWebSessionsExcept(originalUser.ID, ""); err != nil {
+			response.JSONServerError(w, r, err)
+			return
+		}
+	}
+
 	response.JSONCreated(w, r, originalUser)
 }
 

+ 4 - 0
internal/cli/reset_password.go

@@ -35,5 +35,9 @@ func resetPassword(store *storage.Storage) {
 		printErrorAndExit(err)
 	}
 
+	if err := store.RemoveUserWebSessionsExcept(user.ID, ""); err != nil {
+		printErrorAndExit(err)
+	}
+
 	fmt.Println("Password changed!")
 }

+ 10 - 0
internal/storage/web_session.go

@@ -226,6 +226,16 @@ func (s *Storage) RemoveUserWebSession(userID int64, sessionID string) error {
 	return nil
 }
 
+// RemoveUserWebSessionsExcept removes all web sessions of the given user except
+// the one identified by keepSessionID. An empty keepSessionID removes them all.
+func (s *Storage) RemoveUserWebSessionsExcept(userID int64, keepSessionID string) error {
+	if _, err := s.db.Exec(`DELETE FROM web_sessions WHERE user_id=$1 AND id<>$2`, userID, keepSessionID); err != nil {
+		return fmt.Errorf(`store: unable to remove web sessions: %v`, err)
+	}
+
+	return nil
+}
+
 // anonymousWebSessionLifetime is how long a web session that never signed in
 // is kept. These are created for every visitor of the login page, including
 // crawlers and scanners that never send the cookie back.

+ 15 - 0
internal/ui/authentication_update.go

@@ -69,6 +69,21 @@ func (h *handler) updateAuthentication(w http.ResponseWriter, r *http.Request) {
 	}
 
 	sess := request.WebSession(r)
+
+	if authSettingsForm.Password != "" {
+		// Sign out every other session, and give this one a new identity in
+		// case its cookie leaked along with the old password.
+		if err := h.store.RemoveUserWebSessionsExcept(user.ID, sess.ID); err != nil {
+			response.HTMLServerError(w, r, err)
+			return
+		}
+
+		if err := authenticateWebSession(w, r, h.store, user); err != nil {
+			response.HTMLServerError(w, r, err)
+			return
+		}
+	}
+
 	sess.SetSuccessMessage(locale.NewPrinter(sess.Language()).Printf("alert.prefs_saved"))
 	response.HTMLRedirect(w, r, h.routePath("/authentication"))
 }

+ 8 - 0
internal/ui/user_update.go

@@ -66,5 +66,13 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	if userForm.Password != "" {
+		// Administrators editing their own account keep their current session.
+		if err := h.store.RemoveUserWebSessionsExcept(selectedUser.ID, request.WebSession(r).ID); err != nil {
+			response.HTMLServerError(w, r, err)
+			return
+		}
+	}
+
 	response.HTMLRedirect(w, r, h.routePath("/users"))
 }