mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
fix(web/security): drop legacy localStorage init in TenantContext
The tenantId useState initializer used to seed itself from localStorage, opening a small mount-window where a child component could fetch with a tampered tenantId before the auth effect overrode it from user.tenantId. No remaining feature reads from this localStorage key (the only setTenantId call sites are inside TenantContext itself), so the read + the corresponding write are dead. Initialize empty, clear any stale LS value once on mount, and rely solely on the JWT-derived user.tenantId. Backend MUST still derive tenantId from the JWT claim, not from query/body params - this change is defense-in-depth, not the primary guarantee. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,19 +10,34 @@ interface TenantContextType {
|
||||
|
||||
const TenantContext = createContext<TenantContextType | undefined>(undefined);
|
||||
|
||||
const LEGACY_FAKE_TENANT_ID = 'deadbeef-dead-beef-dead-beefdeadbeef';
|
||||
// Legacy localStorage key — older builds wrote the active tenantId here so it
|
||||
// survived reloads. The current model treats the JWT `/api/auth/me` response as
|
||||
// the single source of truth, so we never read from this key anymore. We still
|
||||
// proactively clear it on mount to remove a stale value that an XSS payload or
|
||||
// a curious user could otherwise plant — this closes a small mount-window risk
|
||||
// where a child component could fetch with a tampered tenantId before the auth
|
||||
// effect overrides it. The defense is belt-and-braces: the backend MUST also
|
||||
// derive tenantId from the JWT, never from query/body params.
|
||||
const LEGACY_TENANT_ID_LS_KEY = 'tenantId';
|
||||
|
||||
export function TenantProvider({ children }: { children: React.ReactNode }) {
|
||||
const { user, isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
const [tenantId, setTenantId] = useState<string>(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = localStorage.getItem('tenantId');
|
||||
// Migrate away from the old fake placeholder ID
|
||||
return (stored && stored !== LEGACY_FAKE_TENANT_ID) ? stored : '';
|
||||
// Always start empty. Consumers across the app already gate fetches on
|
||||
// `if (!tenantId) return;` so the empty initial render is the expected path.
|
||||
const [tenantId, setTenantId] = useState<string>('');
|
||||
|
||||
// One-shot cleanup of any stale localStorage value left over from older builds.
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
if (localStorage.getItem(LEGACY_TENANT_ID_LS_KEY) !== null) {
|
||||
localStorage.removeItem(LEGACY_TENANT_ID_LS_KEY);
|
||||
}
|
||||
} catch {
|
||||
// localStorage may be disabled (privacy mode) — ignore.
|
||||
}
|
||||
return '';
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Update tenant ID from authenticated user
|
||||
useEffect(() => {
|
||||
@@ -42,13 +57,6 @@ export function TenantProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}, [user, isAuthenticated, isLoading, tenantId]);
|
||||
|
||||
// Save tenant ID to localStorage
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('tenantId', tenantId);
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
return (
|
||||
<TenantContext.Provider value={{ tenantId, setTenantId }}>
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user