Skip to content

fix: address five medium-severity auth lifecycle security gaps#52

Draft
Copilot wants to merge 2 commits intomainfrom
copilot/fix-auth-lifecycle-gaps
Draft

fix: address five medium-severity auth lifecycle security gaps#52
Copilot wants to merge 2 commits intomainfrom
copilot/fix-auth-lifecycle-gaps

Conversation

Copy link

Copilot AI commented Mar 18, 2026

Five medium-severity security issues across auth lifecycle, payload validation, and data handling: potential OIDC JWT leakage to logs, unvalidated message payloads, SSRF/DoS vectors in the offscreen document, incomplete persistent storage cleanup on account deletion, and bypassed token validation on restoration.

Changes

Token leak in logs (AuthService.ts)

  • Removed responseUrl from the error log in authenticateWithGoogle() — the URL's hash fragment may contain the OIDC ID token (JWT with email, name, Google ID)

Unvalidated assetId payload (service-worker.ts)

  • Added ASSET_ID_PATTERN = /^screenshot_\d+_[a-z0-9]+$/ constant and validates assetId before IndexedDB access in handleAssetUpload
  • Removed isManualRetry=true from addToQueue() — a crafted UPLOAD_ASSET message could otherwise unpause a queue the user intentionally paused (e.g. due to insufficient credits)

Unvalidated image data URLs (offscreen.ts)

  • Added shared validateImagePayload() helper (called in both cropImage and addWatermark) that enforces dataUrl.startsWith('data:image/') and caps dimensions at MAX_CANVAS_DIMENSION = 16384
  • Prevents arbitrary external URL loads via new Image() and DoS through oversized canvas allocations
function validateImagePayload(dataUrl: string, width: number, height: number): void {
  if (!dataUrl.startsWith('data:image/')) throw new Error('Invalid image data URL');
  if (width > MAX_CANVAS_DIMENSION || height > MAX_CANVAS_DIMENSION)
    throw new Error('Image dimensions exceed maximum allowed size');
}

Incomplete account deletion (NumbersApiManager.ts)

  • Added deleteAccount() to NumbersApiManager — the existing AuthService.deleteAccount() only cleared the in-memory token; email, username, and token persisted in chrome.storage.local
async deleteAccount(): Promise<void> {
  await this.auth.deleteAccount();  // API call + in-memory clear
  await storageService.clearAuth(); // persistent storage clear
}

Auth token restoration bypassing validation (service-worker.ts)

  • Replaced two instances of direct numbersApi.setAuthToken(storedAuth.token) with await numbersApi.initialize(), which validates the restored token against the API and clears it if expired/revoked
Original prompt

This section details on the original issue you should resolve

<issue_title>[Security][Medium] Auth lifecycle gaps: token leak in logs, unvalidated payloads, incomplete account deletion</issue_title>
<issue_description>## Summary

Five medium-severity security findings across auth lifecycle, payload validation, and data handling that are not covered by existing issues.

Finding 1: Google Response URL (With Potential Token) Logged on Error

File: src/services/AuthService.ts (line 126)

When the Google auth flow fails to extract an id_token, the full responseUrl (which may contain the token in the hash fragment) is logged: console.error('No id_token found in response', responseUrl). If the token is present but parsing fails for an edge case, the Google OIDC ID token (a JWT containing email, name, Google ID) is written to console.

Suggested Fix: console.error('No id_token found in Google auth response') — do not log the URL.

Finding 2: handleAssetUpload Trusts Payload assetId Without Validation

File: src/background/service-worker.ts (lines 643-659)

The handleAssetUpload function accepts payload.assetId and passes it directly to IndexedDB. No type/format validation is performed. Additionally, calling addToQueue(asset, true) with isManualRetry=true unpauses the entire upload queue — a single crafted message could unpause uploads the user intentionally paused (e.g., due to insufficient credits).

Note: Distinct from issue #19 (sender validation) — this is about payload content validation and the queue unpause side-effect.

Suggested Fix: Validate assetId matches expected format (/^screenshot_\d+_[a-z0-9]+$/). Do not pass isManualRetry=true for programmatic retries.

Finding 3: Offscreen Document Accepts Unvalidated Image Data URLs

File: src/offscreen/offscreen.ts (lines 29-47)

The offscreen document accepts ADD_WATERMARK and CROP_IMAGE messages and passes dataUrl directly to new Image() via loadImage() without validating it starts with data:image/. An attacker could cause the offscreen document to make arbitrary image requests to external URLs. Crop coordinates are also unvalidated, enabling denial-of-service through extremely large canvas allocations.

Suggested Fix: Validate dataUrl.startsWith('data:image/'). Add maximum canvas dimension check (e.g., 16384x16384).

Finding 4: deleteAccount() Does Not Clear Persistent Local Storage

File: src/services/AuthService.ts (lines 175-178)

deleteAccount() calls this.clearAuth() which only clears the in-memory token. It does not call storageService.clearAuth(). After account deletion, the user's email, username, and (now-invalid) auth token persist in chrome.storage.local.

Suggested Fix: Call storageService.clearAuth() within deleteAccount(), or ensure the caller chain includes persistent storage cleanup.

Finding 5: Auth Token Restoration Bypasses Existing Validation Logic

File: src/background/service-worker.ts (lines 330-337, 548-556)

In two places, the service worker restores auth tokens from chrome.storage.local directly into the API client without calling numbersApi.initialize() which validates the token by fetching the user profile. An expired or revoked token would be silently used for upload attempts.

Note: Distinct from issue #11 (missing token refresh mechanism) — this is about bypassing existing validation during restoration.

Suggested Fix: Replace direct token setting with await numbersApi.initialize() which includes validation.</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

- Finding 1: Remove responseUrl from error log in AuthService to prevent JWT leakage
- Finding 2: Validate assetId format and remove isManualRetry side-effect in handleAssetUpload
- Finding 3: Validate dataUrl prefix and add max canvas dimension check in offscreen.ts
- Finding 4: Add deleteAccount() to NumbersApiManager that clears persistent storage
- Finding 5: Replace direct token restoration with numbersApi.initialize() for validation

Co-authored-by: numbers-official <181934381+numbers-official@users.noreply.github.com>
Copilot AI changed the title [WIP] [Security] Fix auth lifecycle gaps related to token leak and validation fix: address five medium-severity auth lifecycle security gaps Mar 18, 2026
Copilot AI requested a review from numbers-official March 18, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security][Medium] Auth lifecycle gaps: token leak in logs, unvalidated payloads, incomplete account deletion

2 participants