π Problem Statement
IssueScope stores API keys in memory (explicitly advertised: "API keys are stored in memory and never sent to a backend server"). However, React Query's default error handler (console.error) logs the full request config on failed API calls. When an OpenRouter request fails (rate limit, invalid key, network timeout), the error object logged by React Query includes the Authorization: Bearer <key> header in the request config dump.
Reproduction:
- Enter a valid OpenRouter key and trigger analysis.
- Introduce a rate limit condition (rapid re-submission or throttled network).
- Open DevTools β Console.
- Observe
AxiosError or FetchError with config.headers.Authorization: "Bearer sk-or-v1-..." printed in plaintext.
This leaks the key to anyone who later inspects browser history, has access to the user's DevTools console session export, or is screen-sharing during use.
Proposed Fix
1. Sanitize error objects before they reach React Query
// src/lib/queryClient.ts (or wherever QueryClient is instantiated)
import { QueryClient } from '@tanstack/react-query';
const sanitizeError = (error: unknown): unknown => {
if (error instanceof Error && 'config' in error) {
// Strip authorization headers from logged error
const sanitized = { ...error } as Record<string, unknown>;
if (sanitized.config && typeof sanitized.config === 'object') {
const config = { ...(sanitized.config as Record<string, unknown>) };
if (config.headers && typeof config.headers === 'object') {
config.headers = { ...(config.headers as Record<string, string>) };
delete (config.headers as Record<string, string>)['Authorization'];
}
sanitized.config = config;
}
return sanitized;
}
return error;
};
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
onError: (error) => {
console.error('[IssueScope] Query error:', sanitizeError(error));
},
},
},
});
2. Use a custom fetch wrapper that strips auth headers from error objects
// src/lib/openrouterClient.ts
export const createOpenRouterClient = (apiKey: string) => ({
analyze: async (prompt: string) => {
try {
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ model: '...', messages: [{ role: 'user', content: prompt }] }),
});
if (!response.ok) {
// Throw error WITHOUT the apiKey in scope
throw new Error(`OpenRouter API error: ${response.status} ${response.statusText}`);
}
return response.json();
} catch (err) {
// Re-throw with key scrubbed
throw new Error(err instanceof Error ? err.message : 'Analysis failed');
}
},
});
Files to Modify
| File |
Change |
src/lib/queryClient.ts |
Add sanitizeError wrapper to onError handlers |
src/lib/openrouterClient.ts |
New β centralized client that sanitizes errors before propagation |
src/hooks/useAnalysis.ts |
Use new client wrapper |
Suggested labels: bug, security, api
I would like to work on this. Could you please assign it to me?
π Problem Statement
IssueScope stores API keys in memory (explicitly advertised: "API keys are stored in memory and never sent to a backend server"). However, React Query's default error handler (
console.error) logs the full request config on failed API calls. When an OpenRouter request fails (rate limit, invalid key, network timeout), the error object logged by React Query includes theAuthorization: Bearer <key>header in the request config dump.Reproduction:
AxiosErrororFetchErrorwithconfig.headers.Authorization: "Bearer sk-or-v1-..."printed in plaintext.This leaks the key to anyone who later inspects browser history, has access to the user's DevTools console session export, or is screen-sharing during use.
Proposed Fix
1. Sanitize error objects before they reach React Query
2. Use a custom fetch wrapper that strips auth headers from error objects
Files to Modify
src/lib/queryClient.tssanitizeErrorwrapper toonErrorhandlerssrc/lib/openrouterClient.tssrc/hooks/useAnalysis.tsSuggested labels:
bug,security,apiI would like to work on this. Could you please assign it to me?