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 <avinal.xlvii@gmail.com>
This commit is contained in:
2026-04-20 17:38:35 +05:30
parent f8e49691a1
commit 669d39e1dd
3 changed files with 33 additions and 16 deletions
+3
View File
@@ -13,6 +13,9 @@
"sessions", "sessions",
"storage" "storage"
], ],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; img-src 'self' data:;"
},
"background": { "background": {
"scripts": ["dist/background.js"] "scripts": ["dist/background.js"]
}, },
+9 -3
View File
@@ -1,7 +1,10 @@
import type { Message } from "./types"; import type { Message } from "./types";
const EXTENSION_ORIGIN = chrome.runtime.getURL("").slice(0, -1);
let overlay: HTMLDivElement | null = null; let overlay: HTMLDivElement | null = null;
let iframe: HTMLIFrameElement | null = null; let iframe: HTMLIFrameElement | null = null;
let messageNonce: string | null = null;
function createOverlay(): void { function createOverlay(): void {
overlay = document.createElement("div"); overlay = document.createElement("div");
@@ -21,8 +24,9 @@ function createOverlay(): void {
transition: background 0.15s ease; transition: background 0.15s ease;
`; `;
messageNonce = crypto.randomUUID();
iframe = document.createElement("iframe"); iframe = document.createElement("iframe");
iframe.src = chrome.runtime.getURL("sciezka.html"); iframe.src = chrome.runtime.getURL("sciezka.html") + "#" + messageNonce;
iframe.style.cssText = ` iframe.style.cssText = `
width: 620px; width: 620px;
height: 60px; height: 60px;
@@ -81,6 +85,7 @@ document.addEventListener("keydown", (e) => {
window.addEventListener("message", (event) => { window.addEventListener("message", (event) => {
if (event.source !== iframe?.contentWindow) return; if (event.source !== iframe?.contentWindow) return;
if (!event.data?._nonce || event.data._nonce !== messageNonce) return;
const data = event.data as Message; const data = event.data as Message;
if (data.type === "closeSaka") { if (data.type === "closeSaka") {
@@ -94,8 +99,9 @@ window.addEventListener("message", (event) => {
return; return;
} }
if (data.type === "search" || data.type === "action") { if (data.type === "search" || data.type === "action") {
chrome.runtime.sendMessage(data, (response) => { chrome.runtime.sendMessage(data, (response: unknown) => {
iframe?.contentWindow?.postMessage(response, "*"); const msg = typeof response === "object" && response ? { ...(response as Record<string, unknown>), _nonce: messageNonce } : response;
iframe?.contentWindow?.postMessage(msg, EXTENSION_ORIGIN);
}); });
} }
}); });
+21 -13
View File
@@ -11,6 +11,8 @@ const MODE_LABELS: Record<SearchMode, string> = {
}; };
const METHODS: SearchMethod[] = ["fuzzy", "fulltext", "prefix"]; const METHODS: SearchMethod[] = ["fuzzy", "fulltext", "prefix"];
const MESSAGE_NONCE = location.hash.slice(1);
let currentMode: SearchMode = "tabs"; let currentMode: SearchMode = "tabs";
let currentMethod: SearchMethod = "fuzzy"; let currentMethod: SearchMethod = "fuzzy";
let results: SearchResult[] = []; let results: SearchResult[] = [];
@@ -25,17 +27,17 @@ const root = document.getElementById("sciezka-root") as HTMLDivElement;
function notifyResize(): void { function notifyResize(): void {
const height = Math.min(root.scrollHeight, 520); 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<unknown> { function sendMessage(msg: Message): Promise<unknown> {
return new Promise((resolve) => { return new Promise((resolve) => {
window.parent.postMessage(msg, "*"); window.parent.postMessage({ ...msg, _nonce: MESSAGE_NONCE }, "*");
const handler = (event: MessageEvent) => { const handler = (event: MessageEvent) => {
if (event.source === window.parent) { if (event.source !== window.parent) return;
window.removeEventListener("message", handler); if (!event.data?._nonce || event.data._nonce !== MESSAGE_NONCE) return;
resolve(event.data); window.removeEventListener("message", handler);
} resolve(event.data);
}; };
window.addEventListener("message", handler); window.addEventListener("message", handler);
}); });
@@ -62,11 +64,17 @@ function renderMethodBadge(): void {
} }
function highlightText(text: string, positions: number[], offset: number): string { 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)); const posSet = new Set(positions.map((p) => p - offset).filter((p) => p >= 0 && p < text.length));
return chars let result = "";
.map((c, i) => (posSet.has(i) ? `<mark>${escapeHtml(c)}</mark>` : escapeHtml(c))) let inMark = false;
.join(""); for (let i = 0; i < text.length; i++) {
const matched = posSet.has(i);
if (matched && !inMark) { result += "<mark>"; inMark = true; }
if (!matched && inMark) { result += "</mark>"; inMark = false; }
result += escapeHtml(text[i]);
}
if (inMark) result += "</mark>";
return result;
} }
function escapeHtml(s: string): string { function escapeHtml(s: string): string {
@@ -157,7 +165,7 @@ function activateResult(index: number): void {
: { type: "action", action: "open", id: item.url }; : { type: "action", action: "open", id: item.url };
sendMessage(msg); sendMessage(msg);
window.parent.postMessage({ type: "closeSaka" }, "*"); window.parent.postMessage({ type: "closeSaka", _nonce: MESSAGE_NONCE }, "*");
} }
async function doSearch(): Promise<void> { async function doSearch(): Promise<void> {
@@ -183,7 +191,7 @@ input.addEventListener("input", () => {
document.addEventListener("keydown", (e) => { document.addEventListener("keydown", (e) => {
if (e.key === "Escape") { if (e.key === "Escape") {
window.parent.postMessage({ type: "closeSaka" }, "*"); window.parent.postMessage({ type: "closeSaka", _nonce: MESSAGE_NONCE }, "*");
return; return;
} }
@@ -209,7 +217,7 @@ document.addEventListener("keydown", (e) => {
const result = results[selectedIndex]; const result = results[selectedIndex];
if (result && result.item.type !== "tabs") { if (result && result.item.type !== "tabs") {
sendMessage({ type: "action", action: "open", id: result.item.url, newTab: true }); 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 { } else {
activateResult(selectedIndex); activateResult(selectedIndex);