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);