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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYpHw7VYW1d2HwfL9EZKx4

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
LeChristopher Blackwell
2026-07-08 05:38:23 -07:00
committed by GitHub
parent 123d244a51
commit 5884573a69
2 changed files with 14 additions and 2 deletions

View File

@@ -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);
}

View File

@@ -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;