Is HolyTrick safe?
HolyTrick is high risk. When you ask HolyTrick to change a page, its content script sends sanitized page HTML and your prompt to api.holytrick.com/getTrick. A // EXEC marker in the response makes the background script eval the rest via Runtime.evaluate.
Who publishes it1 other listing from the same operator, none carrying a finding
1 other listing from the same operator, none carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
1 other listing published from this account, 1k+ users between them, none of them carrying a finding.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
HolyTrick Runs Server-Returned Code in Active Tabs
When you ask HolyTrick to change a page, its content script sends sanitized page HTML and your prompt to api.holytrick.com/getTrick.
A // EXEC marker in the response makes the background script eval the rest via Runtime.evaluate.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
You submit a command in HolyTrick while a webpage is open.
The action can start from the extension UI or a saved script action.
The extension sends page content to HolyTrick and can run marked server-returned code in that tab.
The shipped background script checks the response for // EXEC and passes that response text into Chrome debugger Runtime.evaluate.
- Your HolyTrick commandHide the recommendation section on the right side of this page (illustrative)
This is the instruction you typed for the page change.
- Current page HTML<form><input name="email" value="alex@example.com"><button>Continue</button></form> (illustrative)
This can include visible text, form values, element names, and the page structure from the site you are viewing.
- Selected element DOM{"Dom":"<button id="checkout">Pay now</button>","selector":"#checkout"} (illustrative)
If you select page elements, their DOM and selector are sent with the request.
- Extension metadata{"version":"0.1.4","apiKey":""} (illustrative)
The request includes metadata that helps the service handle this extension version.
Dynamic analysis confirmed this endpoint path and confirmed that a response containing // EXEC is passed to Runtime.evaluate.
- Content-Type
- application/json
Shipped code path from page snapshot to debugger evaluation
Readable equivalent: sanitize the active page HTML
content-scripts/ui.jsfunction sanitizePageHtml(html) { const config = { ALLOWED_TAGS: ["div", "p", "span", "a", "img", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "table", "tr", "td", "th", "thead", "tbody", "input", "button", "form", "label", "select", "option", "textarea", "br", "hr", "strong", "em", "code", "pre", "blockquote"], ALLOWED_ATTR: ["id", "class", "name", "value", "type", "placeholder", "for", "alt", "title"], ALLOW_DATA_ATTR: false, ALLOW_UNKNOWN_PROTOCOLS: false, ALLOW_ARIA_ATTR: false, FORBID_TAGS: ["style", "script"], FORBID_ATTR: ["style", "href", "src"], KEEP_CONTENT: true }; DOMPurify.addHook("uponSanitizeElement", (node, data) => { if (node instanceof HTMLElement) { const style = node.getAttribute("style"); if (style && style.toLowerCase().includes("display:none")) { node.parentNode?.removeChild(node); return; } if (node.hasAttribute("hidden")) { node.parentNode?.removeChild(node); return; } if (data.tagName === "textarea" && node.textContent) { node.textContent = node.textContent.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, ""); node.textContent = node.textContent.replace(/style\s*=\s*["'][^"']*["']/gi, ""); } } }); function normalizeWhitespace(value) { let normalized = value.replace(/\s+/g, " "); normalized = normalized.trim(); normalized = normalized.replace(/\s*([,.!?:;])\s*/g, "$1 ").trim(); normalized = normalized.replace(/([,.!?:;])([a-zA-Z0-9])/g, "$1 $2"); return normalized; } const sanitized = DOMPurify.sanitize(html, config); return normalizeWhitespace(sanitized);}Readable equivalent: send the prompt and page snapshot to the background
content-scripts/ui.jsconst sendFeature = async () => { if (!promptText) return; setLoading(buildLoadingState()); closeElementPicker(); const sanitizedHtml = sanitizePageHtml(document.body.innerHTML); setCurrentHtml(sanitizedHtml); await browser.runtime.sendMessage({ action: "ht_send_feature", data: [ ...messages.map(message => ({ role: message.role, content: message.code || message.text })), { role: "user", content: promptText } ], html: sanitizedHtml, extra: { selectedElements: selectedElements.map(element => ({ Dom: element.Dom, selector: element.selector })) } }); setProgressText(""); setPromptText(""); setRunning(true); setSelectedElements([]); setMessages([...messages, { role: "user", text: promptText, status: "running" }]);};Readable equivalent: POST to getTrick and collect streamed content
background.jsasync function sendToHolyTrick(userCommand, html, extra, onProgress, onFinish, onError) { try { const account = await getAccount(); const token = account ? await getAuthToken() : ""; const response = await fetch("https://api.holytrick.com/getTrick", { method: "POST", body: JSON.stringify({ user_command: userCommand, html, extra }), headers: { "Content-Type": "application/json", Authorization: account ? `Bearer ${token}` : "" } }); if (!response.ok) { onError?.((await response.text()) || "unknown error"); return; } const reader = response.body?.getReader(); const decoder = new TextDecoder("utf-8"); let buffer = ""; let output = ""; for (;;) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.startsWith("data: ")) { const data = line.slice(6); if (data === "[DONE]") continue; const chunk = JSON.parse(data).choices[0]?.delta?.content || ""; if (chunk) { output += chunk; onProgress?.(output); } } } } onFinish?.(output); } catch (error) { console.error("Error sending data:", error); onError?.(error); }}Readable equivalent: evaluate returned code through Chrome debugger
background.jsasync function evaluateInTab(tabId, remoteCode) { await browser.debugger.attach({ tabId }, "1.3"); if (Array.isArray(remoteCode)) { const results = []; for (const code of remoteCode) { const result = await browser.debugger.sendCommand( { tabId }, "Runtime.evaluate", { expression: helperPrefix + code, returnByValue: true } ); track("EXEC_CODE"); results.push(result); } await browser.debugger.detach({ tabId }); return results; } const result = await browser.debugger.sendCommand( { tabId }, "Runtime.evaluate", { expression: helperPrefix + remoteCode, returnByValue: true } ); await browser.debugger.detach({ tabId }); track("EXEC_CODE"); return result;}Readable equivalent: handle a marked server response
background.jsbrowser.runtime.onMessage.addListener(async (message, sender, sendResponse) => { if (message.action === "ht_send_feature" && sender.tab) { storage.getItem("local:apiKey").then(apiKey => { sendToHolyTrick( message.data, message.html, { apiKey: apiKey || cachedApiKey || "", version: extensionVersion, ...message.extra }, progress => browser.tabs.sendMessage(sender.tab.id, { action: "ht_send_feature_progress", finished: false, data: progress }), async responseText => { track("SEND_FEATURE"); await browser.tabs.sendMessage(sender.tab.id, { action: "ht_send_feature_progress", finished: true, data: responseText }); try { if (responseText?.includes("// EXEC")) { const remoteCode = responseText.substring(responseText.indexOf("// EXEC")); const evaluation = await evaluateInTab(sender.tab.id, remoteCode); await browser.tabs.sendMessage(sender.tab.id, { action: "ht_send_feature_finished", data: evaluation?.result?.value, code: remoteCode }); if (evaluation?.result?.value?.success) { track(`EXEC_CODE_${evaluation.result.value.success ? "SUCCESS" : "FAILED"}`); } } else { await browser.tabs.sendMessage(sender.tab.id, { action: "ht_send_feature_finished", data: { success: false, message: responseText.replace("REJECTED: ", "") } }); } } catch (error) { track("EXEC_CODE_FAILED", { error: typeof error === "object" ? JSON.stringify(error) : error }); await browser.tabs.sendMessage(sender.tab.id, { action: "ht_send_feature_finished", data: { success: false, message: typeof error === "object" ? JSON.stringify(error) : error } }); } }, error => browser.tabs.sendMessage(sender.tab.id, { action: "ht_send_feature_finished", data: { message: error, success: false } }) ); sendResponse({ action: "hello" }); }); return true; }});- api.holytrick.com
Receives the getTrick POST containing the user command, sanitized page HTML, and extra metadata; its streamed response controls whether the debugger evaluation path is used.
What it can do
Permissions this extension asks for, as declared in version 0.1.4. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Read and change your data on every site you visit
<all_urls>
Store data in your browser
storage
See the address and title of every tab you have open
tabs
Attach to pages with the browser's debugger, which can read and rewrite anything on them
debugger
Sign you in with your Google account
identity