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;