From 5884573a6975f53381759a28280afd9c8bb332c4 Mon Sep 17 00:00:00 2001 From: LeChristopher Blackwell Date: Wed, 8 Jul 2026 05:38:23 -0700 Subject: [PATCH] fix: validate X-Refreshed-Token before storing it as the auth token (#971) * fix: validate X-Refreshed-Token before storing it as the auth token authenticatedFetch stored any X-Refreshed-Token response header value straight into localStorage. A malformed or injected header value would silently overwrite the stored auth token, breaking every subsequent authenticated request. Only accept values that match the JWT shape the server actually issues (three base64url segments). * fix: apply refreshed-token validation to file upload path Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GYpHw7VYW1d2HwfL9EZKx4 --------- Co-authored-by: Claude Fable 5 --- src/components/file-tree/hooks/useFileTreeUpload.ts | 3 ++- src/utils/api.js | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/file-tree/hooks/useFileTreeUpload.ts b/src/components/file-tree/hooks/useFileTreeUpload.ts index 37cd936d..dbd93700 100644 --- a/src/components/file-tree/hooks/useFileTreeUpload.ts +++ b/src/components/file-tree/hooks/useFileTreeUpload.ts @@ -3,6 +3,7 @@ import type { DragEvent } from 'react'; import { IS_PLATFORM } from '../../../constants/config'; import type { Project } from '../../../types/app'; +import { isValidRefreshedToken } from '../../../utils/api'; import { MAX_FILE_UPLOAD_COUNT, MAX_FILE_UPLOAD_SIZE_BYTES, @@ -131,7 +132,7 @@ const uploadFormDataWithProgress = ( xhr.onload = () => { const refreshedToken = xhr.getResponseHeader('X-Refreshed-Token'); - if (refreshedToken) { + if (isValidRefreshedToken(refreshedToken)) { localStorage.setItem('auth-token', refreshedToken); } diff --git a/src/utils/api.js b/src/utils/api.js index 7502af91..e5eb3fbc 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -1,5 +1,16 @@ import { IS_PLATFORM } from "../constants/config"; +// Only accept a refreshed token that has this app's issued JWT shape +// (three base64url segments). An attacker-injected/malformed header value +// must never overwrite the stored auth token. +/** + * @param {unknown} token + * @returns {token is string} + */ +export const isValidRefreshedToken = (token) => + typeof token === 'string' && + /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token); + // Utility function for authenticated API calls export const authenticatedFetch = (url, options = {}) => { const token = localStorage.getItem('auth-token'); @@ -23,7 +34,7 @@ export const authenticatedFetch = (url, options = {}) => { }, }).then((response) => { const refreshedToken = response.headers.get('X-Refreshed-Token'); - if (refreshedToken) { + if (isValidRefreshedToken(refreshedToken)) { localStorage.setItem('auth-token', refreshedToken); } return response;