fix: follow-up to PR #234 (remove accidental file, revert from document.write to equiv. srcdoc) (#248)
* chore: remove tamper-detection.ts (not meant to be submitted) Follow-up to PR #234. This file was accidentally included. * revert to srcdoc Follow-up to PR #234 per domfarolino's review feedback. * regen * regen
This commit is contained in:
parent
9a9ecf27d4
commit
ba550f1aeb
3 changed files with 6 additions and 343 deletions
|
|
@ -95,21 +95,9 @@ window.addEventListener("message", async (event) => {
|
|||
inner.setAttribute("allow", allowAttribute);
|
||||
}
|
||||
if (typeof html === "string") {
|
||||
// Use document.write instead of srcdoc for WebGL compatibility.
|
||||
// srcdoc creates an opaque origin which prevents WebGL canvas updates
|
||||
// from being displayed properly. document.write preserves the sandbox
|
||||
// origin, allowing WebGL to work correctly.
|
||||
// CSP is enforced via HTTP headers on this page (sandbox.html).
|
||||
const doc = inner.contentDocument || inner.contentWindow?.document;
|
||||
if (doc) {
|
||||
doc.open();
|
||||
doc.write(html);
|
||||
doc.close();
|
||||
} else {
|
||||
// Fallback to srcdoc if document is not accessible
|
||||
console.warn("[Sandbox] document.write not available, falling back to srcdoc");
|
||||
inner.srcdoc = html;
|
||||
}
|
||||
inner.srcdoc = html;
|
||||
} else {
|
||||
console.error("[Sandbox] Missing or invalid HTML content in sandbox-resource-ready notification.");
|
||||
}
|
||||
} else {
|
||||
if (inner && inner.contentWindow) {
|
||||
|
|
|
|||
|
|
@ -1,325 +0,0 @@
|
|||
/**
|
||||
* Tamper Detection Module for MCP-UI Sandbox
|
||||
*
|
||||
* Generates a script that monitors security-sensitive JavaScript APIs
|
||||
* and emits notifications when guest code accesses them.
|
||||
*/
|
||||
|
||||
export type TamperRuleTarget =
|
||||
| "window"
|
||||
| "Element.prototype"
|
||||
| "Document.prototype"
|
||||
| "Node.prototype"
|
||||
| "HTMLElement.prototype"
|
||||
| "EventTarget.prototype"
|
||||
| "History.prototype"
|
||||
| "Location.prototype";
|
||||
|
||||
export type TamperRuleType = "getter" | "method" | "both";
|
||||
export type TamperSeverity = "low" | "medium" | "high" | "critical";
|
||||
|
||||
export interface TamperRule {
|
||||
target: TamperRuleTarget;
|
||||
property: string;
|
||||
type: TamperRuleType;
|
||||
severity: TamperSeverity;
|
||||
}
|
||||
|
||||
export const TAMPER_DETECTED_NOTIFICATION =
|
||||
"ui/notifications/tamper-detected" as const;
|
||||
|
||||
/**
|
||||
* Default rules for security-sensitive APIs.
|
||||
* - critical: Access is blocked (returns undefined / throws)
|
||||
* - high/medium/low: Access is allowed but an alert is emitted
|
||||
*/
|
||||
export const DEFAULT_TAMPER_RULES: TamperRule[] = [
|
||||
// === CRITICAL (blocked) ===
|
||||
// Escape attempts
|
||||
{ target: "window", property: "top", type: "getter", severity: "critical" },
|
||||
{
|
||||
target: "window",
|
||||
property: "parent",
|
||||
type: "getter",
|
||||
severity: "critical",
|
||||
},
|
||||
{
|
||||
target: "window",
|
||||
property: "opener",
|
||||
type: "getter",
|
||||
severity: "critical",
|
||||
},
|
||||
{
|
||||
target: "window",
|
||||
property: "frameElement",
|
||||
type: "getter",
|
||||
severity: "critical",
|
||||
},
|
||||
// Code execution
|
||||
{ target: "window", property: "eval", type: "method", severity: "critical" },
|
||||
|
||||
// === HIGH (allowed with alert) ===
|
||||
{ target: "window", property: "Function", type: "getter", severity: "high" },
|
||||
// Network/data exfiltration
|
||||
{ target: "window", property: "fetch", type: "method", severity: "high" },
|
||||
{
|
||||
target: "window",
|
||||
property: "XMLHttpRequest",
|
||||
type: "getter",
|
||||
severity: "high",
|
||||
},
|
||||
{ target: "window", property: "WebSocket", type: "getter", severity: "high" },
|
||||
{
|
||||
target: "window",
|
||||
property: "EventSource",
|
||||
type: "getter",
|
||||
severity: "high",
|
||||
},
|
||||
// DOM injection (high-risk methods)
|
||||
{
|
||||
target: "Document.prototype",
|
||||
property: "write",
|
||||
type: "method",
|
||||
severity: "high",
|
||||
},
|
||||
{
|
||||
target: "Document.prototype",
|
||||
property: "writeln",
|
||||
type: "method",
|
||||
severity: "high",
|
||||
},
|
||||
{
|
||||
target: "Element.prototype",
|
||||
property: "insertAdjacentHTML",
|
||||
type: "method",
|
||||
severity: "high",
|
||||
},
|
||||
|
||||
// === MEDIUM (allowed with alert) ===
|
||||
// Storage access
|
||||
{
|
||||
target: "window",
|
||||
property: "localStorage",
|
||||
type: "getter",
|
||||
severity: "medium",
|
||||
},
|
||||
{
|
||||
target: "window",
|
||||
property: "sessionStorage",
|
||||
type: "getter",
|
||||
severity: "medium",
|
||||
},
|
||||
{
|
||||
target: "window",
|
||||
property: "indexedDB",
|
||||
type: "getter",
|
||||
severity: "medium",
|
||||
},
|
||||
{ target: "window", property: "caches", type: "getter", severity: "medium" },
|
||||
// DOM injection (property-based)
|
||||
{
|
||||
target: "Element.prototype",
|
||||
property: "innerHTML",
|
||||
type: "both",
|
||||
severity: "medium",
|
||||
},
|
||||
{
|
||||
target: "Element.prototype",
|
||||
property: "outerHTML",
|
||||
type: "both",
|
||||
severity: "medium",
|
||||
},
|
||||
// DOM traversal
|
||||
{
|
||||
target: "Node.prototype",
|
||||
property: "parentNode",
|
||||
type: "getter",
|
||||
severity: "medium",
|
||||
},
|
||||
{
|
||||
target: "Node.prototype",
|
||||
property: "parentElement",
|
||||
type: "getter",
|
||||
severity: "medium",
|
||||
},
|
||||
{
|
||||
target: "Element.prototype",
|
||||
property: "closest",
|
||||
type: "method",
|
||||
severity: "medium",
|
||||
},
|
||||
// History manipulation
|
||||
{ target: "window", property: "history", type: "getter", severity: "medium" },
|
||||
{
|
||||
target: "window",
|
||||
property: "location",
|
||||
type: "getter",
|
||||
severity: "medium",
|
||||
},
|
||||
|
||||
// === LOW (allowed with alert) ===
|
||||
// Message passing
|
||||
{
|
||||
target: "window",
|
||||
property: "postMessage",
|
||||
type: "method",
|
||||
severity: "low",
|
||||
},
|
||||
// DOM queries
|
||||
{
|
||||
target: "Document.prototype",
|
||||
property: "querySelector",
|
||||
type: "method",
|
||||
severity: "low",
|
||||
},
|
||||
{
|
||||
target: "Document.prototype",
|
||||
property: "querySelectorAll",
|
||||
type: "method",
|
||||
severity: "low",
|
||||
},
|
||||
{
|
||||
target: "Document.prototype",
|
||||
property: "getElementById",
|
||||
type: "method",
|
||||
severity: "low",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Generates the tamper detection script as a string.
|
||||
* This script should be injected into guest HTML before any other scripts run.
|
||||
*/
|
||||
export function generateTamperDetectionScript(
|
||||
rules: TamperRule[] = DEFAULT_TAMPER_RULES,
|
||||
): string {
|
||||
const rulesJson = JSON.stringify(rules);
|
||||
|
||||
return `(function() {
|
||||
'use strict';
|
||||
|
||||
var NOTIFICATION_METHOD = '${TAMPER_DETECTED_NOTIFICATION}';
|
||||
var rules = ${rulesJson};
|
||||
|
||||
function emitTamperAlert(rule, accessType, stackTrace) {
|
||||
try {
|
||||
window.parent.postMessage({
|
||||
jsonrpc: '2.0',
|
||||
method: NOTIFICATION_METHOD,
|
||||
params: {
|
||||
property: rule.target + '.' + rule.property,
|
||||
accessType: accessType,
|
||||
severity: rule.severity,
|
||||
blocked: rule.severity === 'critical',
|
||||
timestamp: Date.now(),
|
||||
stack: stackTrace
|
||||
}
|
||||
}, '*');
|
||||
} catch (e) {
|
||||
// Silently fail if postMessage is unavailable
|
||||
}
|
||||
}
|
||||
|
||||
function shouldBlock(rule) {
|
||||
return rule.severity === 'critical';
|
||||
}
|
||||
|
||||
function wrapProperty(target, prop, rule) {
|
||||
var descriptor = Object.getOwnPropertyDescriptor(target, prop);
|
||||
if (!descriptor) return false;
|
||||
|
||||
var newDescriptor = {
|
||||
configurable: false,
|
||||
enumerable: descriptor.enumerable
|
||||
};
|
||||
|
||||
var wrapped = false;
|
||||
|
||||
if (rule.type === 'getter' || rule.type === 'both') {
|
||||
var originalGetter = descriptor.get || (function() { return descriptor.value; });
|
||||
newDescriptor.get = function() {
|
||||
emitTamperAlert(rule, 'read', new Error().stack);
|
||||
if (shouldBlock(rule)) {
|
||||
return undefined;
|
||||
}
|
||||
return originalGetter.call(this);
|
||||
};
|
||||
wrapped = true;
|
||||
}
|
||||
|
||||
if (rule.type === 'method' && typeof descriptor.value === 'function') {
|
||||
var originalMethod = descriptor.value;
|
||||
newDescriptor.value = function() {
|
||||
emitTamperAlert(rule, 'call', new Error().stack);
|
||||
if (shouldBlock(rule)) {
|
||||
throw new Error('Access to ' + rule.target + '.' + rule.property + ' is blocked');
|
||||
}
|
||||
return originalMethod.apply(this, arguments);
|
||||
};
|
||||
wrapped = true;
|
||||
}
|
||||
|
||||
if (rule.type === 'both' && descriptor.set) {
|
||||
var originalSetter = descriptor.set;
|
||||
newDescriptor.set = function(value) {
|
||||
emitTamperAlert(rule, 'write', new Error().stack);
|
||||
if (shouldBlock(rule)) {
|
||||
return;
|
||||
}
|
||||
return originalSetter.call(this, value);
|
||||
};
|
||||
}
|
||||
|
||||
if (wrapped) {
|
||||
try {
|
||||
Object.defineProperty(target, prop, newDescriptor);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getTargetObject(targetName) {
|
||||
switch (targetName) {
|
||||
case 'window': return window;
|
||||
case 'Element.prototype': return Element.prototype;
|
||||
case 'Document.prototype': return Document.prototype;
|
||||
case 'Node.prototype': return Node.prototype;
|
||||
case 'HTMLElement.prototype': return HTMLElement.prototype;
|
||||
case 'EventTarget.prototype': return EventTarget.prototype;
|
||||
case 'History.prototype': return History.prototype;
|
||||
case 'Location.prototype': return Location.prototype;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Install monitoring
|
||||
for (var i = 0; i < rules.length; i++) {
|
||||
var rule = rules[i];
|
||||
var target = getTargetObject(rule.target);
|
||||
if (target) {
|
||||
wrapProperty(target, rule.property, rule);
|
||||
}
|
||||
}
|
||||
})();`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects a script into HTML content, ensuring it runs before any other scripts.
|
||||
*/
|
||||
export function injectScriptIntoHtml(html: string, script: string): string {
|
||||
const scriptTag = `<script>${script}</script>`;
|
||||
|
||||
// Try <head> first
|
||||
if (html.includes("<head>")) {
|
||||
return html.replace("<head>", `<head>${scriptTag}`);
|
||||
}
|
||||
// Try <body>
|
||||
if (html.includes("<body>")) {
|
||||
return html.replace("<body>", `<body>${scriptTag}`);
|
||||
}
|
||||
// Fallback: prepend
|
||||
return scriptTag + html;
|
||||
}
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -6994,9 +6994,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.46.1.tgz",
|
||||
"integrity": "sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA==",
|
||||
"version": "5.46.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.46.3.tgz",
|
||||
"integrity": "sha512-Y5juST3x+/ySty5tYJCVWa6Corkxpt25bUZQHqOceg9xfMUtDsFx6rCsG6cYf1cA6vzDi66HIvaki0byZZX95A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue