From 5a765ad16940e2cae8c5645e6e9ae596982d0fde Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:11:37 +0000 Subject: [PATCH 1/3] Initial plan From 076701a50d0f758ba48220965266608f163de93a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:13:35 +0000 Subject: [PATCH 2/3] Initial plan: fix 6 security/reliability issues Co-authored-by: numbers-official <181934381+numbers-official@users.noreply.github.com> --- package-lock.json | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9c91664..26a975e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "proofsnap-extension", - "version": "1.1.1", + "version": "1.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "proofsnap-extension", - "version": "1.1.1", + "version": "1.1.4", "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" @@ -49,7 +49,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1156,7 +1155,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1267,7 +1265,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -1709,7 +1706,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -1769,7 +1765,6 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2000,7 +1995,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "dev": true, - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", From f4a2e5d4de958590cd241cf98f775ebb4534e840 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:17:07 +0000 Subject: [PATCH 3/3] Fix 6 security/reliability issues: JSON.parse safety, permissions, race conditions Co-authored-by: numbers-official <181934381+numbers-official@users.noreply.github.com> --- manifest.template.json | 1 - src/background/service-worker.ts | 34 +++++++++++++++++++++++++++++++- src/services/StorageService.ts | 20 +++++++++++++++---- src/services/UploadService.ts | 15 +++++++++++--- 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/manifest.template.json b/manifest.template.json index d31cba5..ae7126c 100644 --- a/manifest.template.json +++ b/manifest.template.json @@ -6,7 +6,6 @@ "permissions": [ "activeTab", "storage", - "tabs", "offscreen", "notifications", "identity", diff --git a/src/background/service-worker.ts b/src/background/service-worker.ts index 479f27b..d9b3a64 100644 --- a/src/background/service-worker.ts +++ b/src/background/service-worker.ts @@ -29,6 +29,8 @@ Promise.all([ updateExtensionBadge(); }); console.log('Upload completion callback registered'); + // Auth is now initialized — safe to start processing any queued uploads + numbersApi.upload.startProcessing(); } catch (error) { console.error('Failed to initialize NumbersApiManager:', error); } @@ -162,6 +164,7 @@ async function handleScreenshotCaptureMessage(message: CaptureScreenshotMessage) let pendingSelectionResolve: ((value: any) => void) | null = null; let pendingSelectionReject: ((reason: any) => void) | null = null; let pendingSelectionFromPopup = false; +let pendingSelectionTimeoutId: ReturnType | null = null; /** * Handle selection mode capture @@ -172,6 +175,22 @@ async function handleSelectionCapture(tab: chrome.tabs.Tab): Promise { throw new Error('No active tab found'); } + // Validate that the tab is on a page that supports content script injection + if (!tab.url?.match(/^https?:\/\//)) { + throw new Error('Selection mode is only supported on web pages with http:// or https:// URLs. Chrome extension pages, local files, and browser pages cannot be captured.'); + } + + // Reject any existing pending selection to avoid resource leaks + if (pendingSelectionReject) { + pendingSelectionReject(new Error('Selection cancelled: a new selection was started')); + pendingSelectionResolve = null; + pendingSelectionReject = null; + } + if (pendingSelectionTimeoutId !== null) { + clearTimeout(pendingSelectionTimeoutId); + pendingSelectionTimeoutId = null; + } + // Inject the selection overlay content script try { await chrome.scripting.executeScript({ @@ -189,11 +208,12 @@ async function handleSelectionCapture(tab: chrome.tabs.Tab): Promise { pendingSelectionReject = reject; // Timeout after 60 seconds - setTimeout(() => { + pendingSelectionTimeoutId = setTimeout(() => { if (pendingSelectionReject) { pendingSelectionReject(new Error('Selection timed out')); pendingSelectionResolve = null; pendingSelectionReject = null; + pendingSelectionTimeoutId = null; } }, 60000); }); @@ -205,6 +225,10 @@ async function handleSelectionCapture(tab: chrome.tabs.Tab): Promise { async function handleSelectionComplete(payload: any) { if (payload.cancelled) { console.log('Selection cancelled:', payload.reason); + if (pendingSelectionTimeoutId !== null) { + clearTimeout(pendingSelectionTimeoutId); + pendingSelectionTimeoutId = null; + } if (pendingSelectionResolve) { pendingSelectionResolve({ cancelled: true, reason: payload.reason }); pendingSelectionResolve = null; @@ -357,6 +381,10 @@ async function handleSelectionComplete(payload: any) { }); // Resolve the pending promise + if (pendingSelectionTimeoutId !== null) { + clearTimeout(pendingSelectionTimeoutId); + pendingSelectionTimeoutId = null; + } if (pendingSelectionResolve) { pendingSelectionResolve({ assetId, @@ -370,6 +398,10 @@ async function handleSelectionComplete(payload: any) { } } catch (error: any) { console.error('Failed to capture selection:', error); + if (pendingSelectionTimeoutId !== null) { + clearTimeout(pendingSelectionTimeoutId); + pendingSelectionTimeoutId = null; + } if (pendingSelectionReject) { pendingSelectionReject(error); pendingSelectionResolve = null; diff --git a/src/services/StorageService.ts b/src/services/StorageService.ts index 2f2fce0..0119264 100644 --- a/src/services/StorageService.ts +++ b/src/services/StorageService.ts @@ -132,9 +132,15 @@ export class StorageService { async getSettings(): Promise { const result = await chrome.storage.local.get('user_settings'); if (result.user_settings) { - const saved = JSON.parse(result.user_settings); - // Merge with defaults to ensure new fields are present - return { ...DEFAULT_SETTINGS, ...saved }; + try { + const saved = JSON.parse(result.user_settings); + // Merge with defaults to ensure new fields are present + return { ...DEFAULT_SETTINGS, ...saved }; + } catch (error) { + console.error('Failed to parse user_settings from storage, resetting to defaults:', error); + await this.setSettings(DEFAULT_SETTINGS); + return DEFAULT_SETTINGS; + } } return DEFAULT_SETTINGS; } @@ -166,7 +172,13 @@ export class StorageService { async getUploadQueueIds(): Promise { const result = await chrome.storage.local.get('upload_queue'); if (result.upload_queue) { - return JSON.parse(result.upload_queue); + try { + return JSON.parse(result.upload_queue); + } catch (error) { + console.error('Failed to parse upload_queue from storage, resetting to empty queue:', error); + await chrome.storage.local.remove('upload_queue'); + return []; + } } return []; } diff --git a/src/services/UploadService.ts b/src/services/UploadService.ts index 21dc47c..ef9455e 100644 --- a/src/services/UploadService.ts +++ b/src/services/UploadService.ts @@ -35,7 +35,8 @@ export class UploadService { } /** - * Restore upload queue from storage on initialization + * Restore upload queue from storage on initialization. + * Does NOT start processing — call startProcessing() after auth is ready. */ private async restoreQueue() { try { @@ -51,14 +52,22 @@ export class UploadService { } this.uploadQueue = assets; console.log(`Restored ${assets.length} assets to upload queue`); - // Auto-start processing if not paused - this.processQueue(); + // Processing is deferred until startProcessing() is called after auth init } } catch (error) { console.error('Failed to restore upload queue:', error); } } + /** + * Start processing the upload queue. + * Must be called explicitly after authentication has been initialized + * to avoid uploading with a missing auth token. + */ + startProcessing(): void { + this.processQueue(); + } + /** * Add an asset to the upload queue * @param asset - The asset to upload