Skip to content

[Security][High] Singleton race condition and plaintext auth token storage #55

@numbers-official

Description

@numbers-official

Summary

Two high-severity security findings discovered during deep audit of the main branch (commit cc3b576):

1. Race Condition in getNumbersApi() Singleton (NumbersApiManager.ts:151-159)

The lazy singleton initialization in getNumbersApi() is not guarded against concurrent calls. If two async callers invoke getNumbersApi() simultaneously before the first await instance.initialize() resolves, two separate NumbersApiManager instances will be created. This can cause:

  • Duplicate token restoration — two parallel getCurrentUser() calls to the API
  • State divergence — one caller holds a reference to a different instance than another
  • Auth inconsistency — if one initialize fails and the other succeeds, some components may see logged-in state while others see logged-out

File: src/services/NumbersApiManager.ts lines 150-159

let instance: NumbersApiManager | null = null;

export async function getNumbersApi(): Promise<NumbersApiManager> {
  if (!instance) {
    instance = new NumbersApiManager();  // Not guarded — second caller can enter here
    await instance.initialize();         // Async gap allows concurrent creation
  }
  return instance;
}

Suggested fix: Assign instance before awaiting, or use a mutex/promise-based lock:

let instance: NumbersApiManager | null = null;
let initPromise: Promise<NumbersApiManager> | null = null;

export function getNumbersApi(): Promise<NumbersApiManager> {
  if (!initPromise) {
    initPromise = (async () => {
      const mgr = new NumbersApiManager();
      await mgr.initialize();
      instance = mgr;
      return mgr;
    })();
  }
  return initPromise;
}

2. Auth Token Stored in Plaintext in chrome.storage.local (StorageService.ts:79-85)

The auth token is stored directly in chrome.storage.local without any encryption or obfuscation. While chrome.storage.local is scoped to the extension, any extension with storage permission that shares the same browser profile can potentially read it if Chrome's storage isolation is compromised. Additionally, local storage is written to disk unencrypted.

File: src/services/StorageService.ts lines 79-85

async setAuth(auth: StoredAuth): Promise<void> {
  await chrome.storage.local.set({
    auth_token: auth.token,   // Plaintext token
    auth_email: auth.email,
    auth_username: auth.username,
  });
}

Suggested fix: Use chrome.storage.session (MV3, in-memory only, cleared on browser close) for the auth token while keeping non-sensitive data in local. Alternatively, encrypt the token before storage using crypto.subtle.

Impact

  • Singleton race can cause intermittent auth failures and state corruption
  • Plaintext token storage increases risk if disk or profile is compromised

Metadata

Metadata

Labels

priority:highHigh prioritysecuritySecurity vulnerability or concern

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions