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
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 invokegetNumbersApi()simultaneously before the firstawait instance.initialize()resolves, two separateNumbersApiManagerinstances will be created. This can cause:getCurrentUser()calls to the APIFile:
src/services/NumbersApiManager.tslines 150-159Suggested fix: Assign
instancebefore awaiting, or use a mutex/promise-based lock:2. Auth Token Stored in Plaintext in chrome.storage.local (StorageService.ts:79-85)
The auth token is stored directly in
chrome.storage.localwithout any encryption or obfuscation. Whilechrome.storage.localis scoped to the extension, any extension withstoragepermission 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.tslines 79-85Suggested fix: Use
chrome.storage.session(MV3, in-memory only, cleared on browser close) for the auth token while keeping non-sensitive data inlocal. Alternatively, encrypt the token before storage usingcrypto.subtle.Impact