Skip to content

[BUG] API Key Stored in localStorage Enables Credential Theft via XSS #1136

Description

@ionfwsrijan

Description

The SecuScan frontend persists the user's API key exclusively in window.localStorage:

// api.ts lines 291-307
export function setStoredApiKey(apiKey: string): void {
  window.localStorage.setItem(STORAGE_KEY, apiKey);
}

export function getStoredApiKey(): string | null {
  return window.localStorage.getItem(STORAGE_KEY);
}

This API key is then sent as the sole authentication credential for every backend request (via the X-Api-Key header). The STORAGE_KEY constant is a static string ('secuscan_api_key' or similar), making it trivial to enumerate.

Why This Is a Critical Vulnerability

1. localStorage is accessible to any JavaScript executing on the same origin. This includes:

  • Third-party analytics scripts
  • Browser extensions with content script injection
  • Any XSS payload (even a reflected XSS in a single parameter)

2. No HttpOnly or Secure flag protection. Unlike HttpOnly cookies which are inaccessible to JavaScript, localStorage items can be read with a single line:

const stolenKey = localStorage.getItem('secuscan_api_key');

This key can then be exfiltrated to an attacker-controlled server.

3. No key rotation or expiry. The API key, once stored, persists indefinitely until the user explicitly clears it. There is no mechanism to rotate the key or enforce session timeouts.

4. The attacker gains full access. With the API key, an attacker can:

  • Start/stop any scan on any target
  • Read all scan findings and vulnerability reports
  • Access the credential vault (decrypted credentials)
  • Download generated reports
  • View and modify user settings
  • Delete scan history

Storage Location Spread

The key is also referenced in multiple components that assume it's always in localStorage:

  • api.ts:getStoredApiKey() — called on every API request
  • Various page components that check getStoredApiKey() to determine auth state
  • The ApiKeySetupScreen component that initially stores the key

Impact

This is the single most critical security vulnerability in SecuScan. It turns a DOM-based XSS (even a minor one in any page) into a complete account compromise. The CVSS score would be approximately 9.1 (Critical):

  • Attack Vector: Network
  • Attack Complexity: Low (requires any XSS or compromised third-party script)
  • Privileges Required: None
  • User Interaction: None
  • Scope: Changed
  • Confidentiality: High
  • Integrity: High
  • Availability: High

Proposed Fix

Short-term (Minimal Code Change)

Replace localStorage with an in-memory variable that survives page refreshes via a session cookie with HttpOnly and Secure flags, set by the backend:

Backend change (backend/secuscan/auth.py):

@router.post("/api/v1/auth/session")
async def create_session(api_key: str = Header(...), response: Response = None):
    # Validate the API key
    if not validate_api_key(api_key):
        raise HTTPException(status_code=401)
    
    # Set HttpOnly session cookie
    session_token = create_session_token(api_key)  # Signed, short-lived
    response.set_cookie(
        key="secuscan_session",
        value=session_token,
        httponly=True,
        secure=True,
        samesite="strict",
        max_age=3600,  # 1 hour
    )
    return {"status": "authenticated"}

Frontend change (frontend/src/api.ts):

// Remove localStorage usage entirely
let sessionToken: string | null = null;

export async function authenticateWithApiKey(apiKey: string): Promise<void> {
  // POST to backend, which returns a Set-Cookie header
  await fetch('/api/v1/auth/session', {
    method: 'POST',
    headers: { 'X-Api-Key': apiKey },
    credentials: 'include',  // Important: send cookies
  });
  sessionToken = 'authenticated';  // Flag, not the actual secret
}

Long-term (Recommended Architecture)

  1. Implement a proper OAuth2 / OpenID Connect flow with short-lived access tokens and refresh tokens.
  2. Store the refresh token in an HttpOnly cookie with SameSite=Strict.
  3. Keep the access token in memory (JS variable) and refresh it before expiry.
  4. Add token rotation — each refresh issues a new refresh token and invalidates the old one.
  5. Enforce key expiry on the backend so that even if leaked, the key is only valid for a limited time.

Additional Hardening

  • Add Content-Security-Policy headers that restrict script sources to prevent third-party script injection.
  • Add a Clear-Site-Data header on logout to clear any residual storage.
  • Monitor for anomalous API key usage (requests from unexpected IPs/user-agents) and auto-rotate on detection.

Metadata

Metadata

Assignees

Labels

priority:mediumImportant issue with normal urgencytype:bugBug fix work category bonus label

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions