mirror of
https://github.com/avinal/sciezka.git
synced 2026-07-03 23:30:09 +05:30
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:
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+19
-11
@@ -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) {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user