Skip to content

bug: OpenRouter API key stored in Zustand in-memory state is logged to browser console via React Query's default onError handler β€” key leaks into browser DevTools console output visible to anyone with physical access to the machineΒ #88

Description

@divyanshim27

πŸ› 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:

  1. Enter a valid OpenRouter key and trigger analysis.
  2. Introduce a rate limit condition (rapid re-submission or throttled network).
  3. Open DevTools β†’ Console.
  4. 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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions