Skip to content

fix: Shadow DOM isolation, API error masking, and incomplete logout cleanup#59

Draft
Copilot wants to merge 2 commits intomainfrom
copilot/fix-content-script-dom-injection
Draft

fix: Shadow DOM isolation, API error masking, and incomplete logout cleanup#59
Copilot wants to merge 2 commits intomainfrom
copilot/fix-content-script-dom-injection

Conversation

Copy link
Copy Markdown

Copilot AI commented Mar 19, 2026

Three medium-severity security issues in the content script, API client, and storage layer: host-page interference with the capture UI, silent masking of non-JSON server errors, and sensitive user data persisting in IndexedDB after logout.

Changes

src/content/selection-overlay.ts — Shadow DOM isolation

  • Overlay, selection box, and instructions tooltip are now mounted inside a closed Shadow DOM (mode: 'closed') on a host <div>
  • Host-page CSS can no longer style/hide the overlay; host-page JS can no longer reach elements via document.getElementById or .shadowRoot
  • Cleanup removes the single shadow host, which implicitly removes all children
// Before — elements appended directly, visible to host page
document.body.appendChild(overlay);

// After — elements isolated inside a closed Shadow DOM
const shadowHost = document.createElement('div');
const shadow = shadowHost.attachShadow({ mode: 'closed' });
shadow.appendChild(overlay);
document.body.appendChild(shadowHost);

src/services/ApiClient.ts — Surface non-JSON server errors

  • 204 No Content now correctly returns {} as T
  • All other responses with a non-application/json content type throw ApiError instead of silently returning {}, surfacing 502/503 HTML error pages and other unexpected responses to callers
// Before
if (!contentType || !contentType.includes('application/json')) {
  return {} as T; // swallowed HTML error pages, empty bodies, etc.
}

// After
if (response.status === 204) return {} as T;
if (!contentType?.includes('application/json')) {
  throw new ApiError(`Unexpected content type: ${contentType}`, response.status);
}

src/services/IndexedDBService.ts + StorageService.ts — Clear IndexedDB on logout

  • Added clearAllAssets() to IndexedDBService — clears the assets object store via a readwrite transaction
  • StorageService.clearAll() now also calls indexedDBService.clearAllAssets(), so screenshots (base64 images), GPS coordinates, and source website URLs are removed from ProofSnapDB at logout
Original prompt

This section details on the original issue you should resolve

<issue_title>[Security][Medium] Content script DOM exposure, silent API response masking, and incomplete logout cleanup</issue_title>
<issue_description>## Summary

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

1. Content Script DOM Injection Without Shadow DOM Isolation (selection-overlay.ts:26-92)

The selection overlay content script injects DOM elements directly into the host page without using Shadow DOM. This means:

  • Host page CSS can style/hide the overlay, confusing the user
  • Host page JavaScript can read, modify, or intercept overlay events
  • A malicious page could detect the overlay and manipulate the selection coordinates

File: src/content/selection-overlay.ts lines 26-92

const overlay = document.createElement('div');
overlay.id = 'proofsnap-selection-overlay';
// Styles set directly on element — no Shadow DOM encapsulation
overlay.style.position = 'fixed';
// ...
document.body.appendChild(overlay);

Suggested fix: Create a Shadow DOM root and attach the overlay inside it:

const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'closed' });
shadow.appendChild(overlay);
document.body.appendChild(host);

2. Silent Acceptance of Non-JSON API Responses (ApiClient.ts:169-173)

When the API returns a non-JSON content type (or no content-type header), the client silently returns {} as T. This masks server errors such as HTML error pages (502/503 gateway errors), XML responses, or empty bodies, causing downstream code to operate on an empty object that does not match the expected type.

File: src/services/ApiClient.ts lines 169-173

const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
  return {} as T;  // Silent masking — no error, no warning
}

Suggested fix: Throw an ApiError for unexpected content types on non-204 responses:

if (response.status === 204) return {} as T;
const contentType = response.headers.get('content-type');
if (!contentType?.includes('application/json')) {
  throw new ApiError(`Unexpected content type: ${contentType}`, response.status);
}

3. clearAll() on Logout Does Not Clear IndexedDB (StorageService.ts:254-256)

The clearAll() method only clears chrome.storage.local but does not clear the IndexedDB ProofSnapDB database. After logout, all captured screenshots (as base64 data URLs), GPS coordinates, and source website URLs remain on disk in IndexedDB. This is a privacy concern if the device is shared or compromised.

File: src/services/StorageService.ts lines 254-256

async clearAll(): Promise<void> {
  await chrome.storage.local.clear();
  // IndexedDB 'ProofSnapDB' is NOT cleared
}

File: src/services/IndexedDBService.ts — has no clearAll() or deleteDatabase() method

Suggested fix: Add an IndexedDB clear method and call it from clearAll():

// In IndexedDBService.ts
async clearAllAssets(): Promise<void> {
  const db = await this.getDB();
  const tx = db.transaction('assets', 'readwrite');
  tx.objectStore('assets').clear();
  await new Promise((resolve, reject) => {
    tx.oncomplete = resolve;
    tx.onerror = reject;
  });
}

// In StorageService.ts clearAll()
async clearAll(): Promise<void> {
  await chrome.storage.local.clear();
  await indexedDBService.clearAllAssets();
}

Impact

  • DOM injection without isolation allows host page interference with capture UI
  • Silent API response masking can hide server errors, leading to data corruption
  • Incomplete logout leaves user screenshots and location data on disk</issue_description>

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


⌨️ Start Copilot coding agent tasks without leaving your editor — available in VS Code, Visual Studio, JetBrains IDEs and Eclipse.

…logout cleanup

- selection-overlay.ts: wrap overlay elements in a closed Shadow DOM to
  prevent host-page CSS/JS interference with the capture UI
- ApiClient.ts: treat 204 No Content gracefully and throw ApiError for
  any other non-JSON content type instead of silently returning {}
- IndexedDBService.ts: add clearAllAssets() to wipe the assets store
- StorageService.ts: call indexedDBService.clearAllAssets() from clearAll()
  so logout removes screenshots, GPS coords, and URLs from IndexedDB

Co-authored-by: numbers-official <181934381+numbers-official@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix content script DOM exposure and incomplete logout cleanup fix: Shadow DOM isolation, API error masking, and incomplete logout cleanup Mar 19, 2026
Copilot AI requested a review from numbers-official March 19, 2026 21:15
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] Content script DOM exposure, silent API response masking, and incomplete logout cleanup

2 participants