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",
"storage"
],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; img-src 'self' data:;"
},
"background": {
"scripts": ["dist/background.js"]
},
+9 -3
View File
@@ -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<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 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<unknown> {
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) ? `<mark>${escapeHtml(c)}</mark>` : 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 += "<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 {
@@ -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<void> {
@@ -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);