fix: dual-key rate limiting for auth + websocket reconnect guard
- Pass client IP (X-Real-IP header) through Flask to daemon for both password login and WebAuthn authenticate-finish endpoints - Rate limiter now checks both IP and username buckets: IP layer catches enumeration/brute-force attacks across multiple usernames; username layer protects against single-account targeting from multiple IPs - Add _wsRefreshing flag to prevent double-scheduling reconnect when onclose fires during token refresh; simplify async IIFE to .then()/.catch() - Reset _wsRefreshing on websocket onopen for safety
This commit is contained in:
@@ -49,6 +49,7 @@ def login():
|
||||
"""
|
||||
try:
|
||||
body = request.get_json(silent=True) or {}
|
||||
body["client_ip"] = request.headers.get("X-Real-IP") or request.remote_addr
|
||||
return _ok(post(POST_AUTH_LOGIN, body))
|
||||
except Exception as exc:
|
||||
logger.error("Login failed: %s", exc)
|
||||
@@ -289,6 +290,7 @@ def webauthn_authenticate_finish():
|
||||
"""
|
||||
try:
|
||||
body = request.get_json(silent=True) or {}
|
||||
body["client_ip"] = request.headers.get("X-Real-IP") or request.remote_addr
|
||||
return _ok(post(POST_AUTH_WEBAUTHN_AUTHENTICATE_FINISH, body))
|
||||
except Exception as exc:
|
||||
logger.error("WebAuthn authenticate finish failed: %s", exc)
|
||||
|
||||
@@ -12,6 +12,7 @@ import { tryRefreshToken } from './api.js?v=12';
|
||||
let _wsConn = null;
|
||||
let _wsReconnectMs = 0;
|
||||
let _wsFailCount = 0;
|
||||
let _wsRefreshing = false;
|
||||
|
||||
/** Direct onMessage handlers — { topics, handler, unsubscribed }[] */
|
||||
const _directHandlers = [];
|
||||
@@ -44,28 +45,24 @@ function _wsConnect() {
|
||||
_wsConn.onopen = () => {
|
||||
_wsReconnectMs = 0;
|
||||
_wsFailCount = 0;
|
||||
_wsRefreshing = false;
|
||||
};
|
||||
|
||||
_wsConn.onclose = () => {
|
||||
if (!window.__auth_token__) return;
|
||||
_wsFailCount++;
|
||||
|
||||
if (_wsFailCount >= 3) {
|
||||
// Attempt token refresh after repeated failures. The reconnect
|
||||
// is handled inside the IIFE to avoid double-scheduling when
|
||||
// refresh succeeds.
|
||||
(async () => {
|
||||
const ok = await tryRefreshToken();
|
||||
if (ok) {
|
||||
_wsFailCount = 0;
|
||||
_wsReconnectMs = 0;
|
||||
_wsConn = null;
|
||||
setTimeout(_wsConnect, 100);
|
||||
} else {
|
||||
_wsReconnectMs = Math.min(_wsReconnectMs * 2 + 1000, 15000);
|
||||
setTimeout(_wsConnect, _wsReconnectMs);
|
||||
}
|
||||
})();
|
||||
if (_wsFailCount >= 3 && !_wsRefreshing) {
|
||||
_wsRefreshing = true;
|
||||
tryRefreshToken().then(ok => {
|
||||
_wsRefreshing = false;
|
||||
_wsFailCount = 0;
|
||||
_wsReconnectMs = 0;
|
||||
_wsConn = null;
|
||||
setTimeout(_wsConnect, 100);
|
||||
}).catch(() => {
|
||||
_wsRefreshing = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user