From 669d39e1ddec6bfa104fea440745a6003d825862 Mon Sep 17 00:00:00 2001 From: Avinal Kumar Date: Mon, 20 Apr 2026 17:38:35 +0530 Subject: [PATCH] fix: postMessage security hardening and highlight grouping Add per-session nonce to all postMessage exchanges between content script and iframe, use targeted origin instead of wildcard, add explicit CSP to manifest. Group consecutive matched characters into single mark elements to fix visual spacing. Assisted-by: Claude Code Signed-off-by: Avinal Kumar --- manifest.json | 3 +++ src/content.ts | 12 +++++++++--- src/sciezka.ts | 34 +++++++++++++++++++++------------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/manifest.json b/manifest.json index c043f26..dec7d11 100644 --- a/manifest.json +++ b/manifest.json @@ -13,6 +13,9 @@ "sessions", "storage" ], + "content_security_policy": { + "extension_pages": "script-src 'self'; object-src 'self'; img-src 'self' data:;" + }, "background": { "scripts": ["dist/background.js"] }, diff --git a/src/content.ts b/src/content.ts index 70d4170..fd6bc6f 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,7 +1,10 @@ import type { Message } from "./types"; +const EXTENSION_ORIGIN = chrome.runtime.getURL("").slice(0, -1); + let overlay: HTMLDivElement | null = null; let iframe: HTMLIFrameElement | null = null; +let messageNonce: string | null = null; function createOverlay(): void { overlay = document.createElement("div"); @@ -21,8 +24,9 @@ function createOverlay(): void { transition: background 0.15s ease; `; + messageNonce = crypto.randomUUID(); iframe = document.createElement("iframe"); - iframe.src = chrome.runtime.getURL("sciezka.html"); + iframe.src = chrome.runtime.getURL("sciezka.html") + "#" + messageNonce; iframe.style.cssText = ` width: 620px; height: 60px; @@ -81,6 +85,7 @@ document.addEventListener("keydown", (e) => { window.addEventListener("message", (event) => { if (event.source !== iframe?.contentWindow) return; + if (!event.data?._nonce || event.data._nonce !== messageNonce) return; const data = event.data as Message; if (data.type === "closeSaka") { @@ -94,8 +99,9 @@ window.addEventListener("message", (event) => { return; } if (data.type === "search" || data.type === "action") { - chrome.runtime.sendMessage(data, (response) => { - iframe?.contentWindow?.postMessage(response, "*"); + chrome.runtime.sendMessage(data, (response: unknown) => { + const msg = typeof response === "object" && response ? { ...(response as Record), _nonce: messageNonce } : response; + iframe?.contentWindow?.postMessage(msg, EXTENSION_ORIGIN); }); } }); diff --git a/src/sciezka.ts b/src/sciezka.ts index 0b6aed9..f1d1934 100644 --- a/src/sciezka.ts +++ b/src/sciezka.ts @@ -11,6 +11,8 @@ const MODE_LABELS: Record = { }; const METHODS: SearchMethod[] = ["fuzzy", "fulltext", "prefix"]; +const MESSAGE_NONCE = location.hash.slice(1); + let currentMode: SearchMode = "tabs"; let currentMethod: SearchMethod = "fuzzy"; let results: SearchResult[] = []; @@ -25,17 +27,17 @@ const root = document.getElementById("sciezka-root") as HTMLDivElement; function notifyResize(): void { const height = Math.min(root.scrollHeight, 520); - window.parent.postMessage({ type: "resize", height }, "*"); + window.parent.postMessage({ type: "resize", _nonce: MESSAGE_NONCE, height }, "*"); } function sendMessage(msg: Message): Promise { return new Promise((resolve) => { - window.parent.postMessage(msg, "*"); + window.parent.postMessage({ ...msg, _nonce: MESSAGE_NONCE }, "*"); const handler = (event: MessageEvent) => { - if (event.source === window.parent) { - window.removeEventListener("message", handler); - resolve(event.data); - } + if (event.source !== window.parent) return; + if (!event.data?._nonce || event.data._nonce !== MESSAGE_NONCE) return; + window.removeEventListener("message", handler); + resolve(event.data); }; window.addEventListener("message", handler); }); @@ -62,11 +64,17 @@ function renderMethodBadge(): void { } function highlightText(text: string, positions: number[], offset: number): string { - const chars = text.split(""); const posSet = new Set(positions.map((p) => p - offset).filter((p) => p >= 0 && p < text.length)); - return chars - .map((c, i) => (posSet.has(i) ? `${escapeHtml(c)}` : escapeHtml(c))) - .join(""); + let result = ""; + let inMark = false; + for (let i = 0; i < text.length; i++) { + const matched = posSet.has(i); + if (matched && !inMark) { result += ""; inMark = true; } + if (!matched && inMark) { result += ""; inMark = false; } + result += escapeHtml(text[i]); + } + if (inMark) result += ""; + return result; } function escapeHtml(s: string): string { @@ -157,7 +165,7 @@ function activateResult(index: number): void { : { type: "action", action: "open", id: item.url }; sendMessage(msg); - window.parent.postMessage({ type: "closeSaka" }, "*"); + window.parent.postMessage({ type: "closeSaka", _nonce: MESSAGE_NONCE }, "*"); } async function doSearch(): Promise { @@ -183,7 +191,7 @@ input.addEventListener("input", () => { document.addEventListener("keydown", (e) => { if (e.key === "Escape") { - window.parent.postMessage({ type: "closeSaka" }, "*"); + window.parent.postMessage({ type: "closeSaka", _nonce: MESSAGE_NONCE }, "*"); return; } @@ -209,7 +217,7 @@ document.addEventListener("keydown", (e) => { const result = results[selectedIndex]; if (result && result.item.type !== "tabs") { sendMessage({ type: "action", action: "open", id: result.item.url, newTab: true }); - window.parent.postMessage({ type: "closeSaka" }, "*"); + window.parent.postMessage({ type: "closeSaka", _nonce: MESSAGE_NONCE }, "*"); } } else { activateResult(selectedIndex);