enforce mandatory X-Session-Id header for access token validation

Session binding was bypassable: if the X-Session-Id header was absent,
validate_token skipped the check entirely, allowing a stolen JWT to be
used without the originating session.

Server-side: reject 401 early in Flask middleware and daemon WebSocket
handler when X-Session-Id is missing, before calling validate_token.
Updated validate_token to always enforce session_id matching for access
tokens (refresh tokens are unaffected as they carry no session_id claim).

Frontend: removed dead if (stored.session_id) guards in api.js since
the header is now always required. Added X-Session-Id to logout request
headers and always store session_id on login/refresh.
This commit is contained in:
2026-07-30 22:55:16 +00:00
parent 43b44ad340
commit b69ca330f4
6 changed files with 39 additions and 28 deletions
+3 -9
View File
@@ -107,9 +107,7 @@ async function tryRefreshToken() {
window.__auth_token__ = tokens.access_token;
sessionStorage.setItem('vw:refresh', tokens.refresh_token);
sessionStorage.setItem('vw:access_ttl', String((json.data.access_ttl || 300) * 1000));
if (tokens.session_id) {
sessionStorage.setItem('vw:session_id', tokens.session_id);
}
sessionStorage.setItem('vw:session_id', tokens.session_id);
if (json.data.user) {
sessionStorage.setItem('vw:user', JSON.stringify(json.data.user));
}
@@ -151,9 +149,7 @@ export async function apiFetch(url, options = {}) {
if (token) {
headers['Authorization'] = 'Bearer ' + token;
const stored = getStoredAuth();
if (stored.session_id) {
headers['X-Session-Id'] = stored.session_id;
}
headers['X-Session-Id'] = stored.session_id;
}
if (body && typeof body === 'object' && !(body instanceof FormData)) {
@@ -171,9 +167,7 @@ export async function apiFetch(url, options = {}) {
if (refreshed) {
const refreshedStored = getStoredAuth();
headers['Authorization'] = 'Bearer ' + getAuthToken();
if (refreshedStored.session_id) {
headers['X-Session-Id'] = refreshedStored.session_id;
}
headers['X-Session-Id'] = refreshedStored.session_id;
const retryRes = await fetch(url, { method, headers, body: options.body, credentials: 'same-origin', ...opts });
if (retryRes.ok) {
const json = await retryRes.json().catch(() => null);