Skip to content

Commit 4e0ed85

Browse files
committed
inject via blob: URLs & provide custom TrustedTypes policy
1. Pre-transform ES modules in background worker (eliminates runtime eval) 2. Inject via blob: URLs instead of inline textContent 3. Bootstrap Trusted Types policy via trusted files:[] injection Increases coverage of where user scripts can run.
1 parent b564dcd commit 4e0ed85

2 files changed

Lines changed: 117 additions & 41 deletions

File tree

src/content-scripts/webmcp-polyfill.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,4 +276,25 @@
276276
ValidationError,
277277
ExecutionError
278278
};
279+
280+
// Create Trusted Types policy for user script injection
281+
if (typeof trustedTypes !== 'undefined') {
282+
try {
283+
window.__agentboardTTPolicy = trustedTypes.createPolicy('agentboard-user-scripts', {
284+
createScriptURL: (url) => {
285+
// Only allow same-origin blob: URLs (defense-in-depth)
286+
// Blob URLs are origin-bound by construction: blob:https://site.com/uuid
287+
const expectedPrefix = `blob:${window.location.origin}/`;
288+
if (url.startsWith(expectedPrefix)) {
289+
return url;
290+
}
291+
throw new TypeError(`AgentBoard policy only allows same-origin blob: URLs (expected ${expectedPrefix})`);
292+
}
293+
});
294+
console.log('[WebMCP] Created Trusted Types policy for user scripts');
295+
} catch (error) {
296+
console.warn('[WebMCP] Could not create Trusted Types policy:', error.message);
297+
console.warn('[WebMCP] User scripts may not work on this site due to Trusted Types');
298+
}
299+
}
279300
})();

src/lib/webmcp/script-injector.ts

Lines changed: 96 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ import log from '../logger';
77
import { ConfigStorage, type UserScript, type UserScriptMetadata } from '../storage/config';
88
import { parseUserScript, matchesUrl, ScriptParsingError } from './script-parser';
99

10+
// Type declarations for Trusted Types policy created in webmcp-polyfill.js
11+
// TrustedScriptURL is the return type from createScriptURL()
12+
type TrustedScriptURL = string & { __brand: 'TrustedScriptURL' };
13+
14+
declare global {
15+
interface Window {
16+
__agentboardTTPolicy?: {
17+
createScriptURL: (url: string) => string | TrustedScriptURL;
18+
};
19+
}
20+
}
21+
1022
const configStorage = ConfigStorage.getInstance();
1123

1224
export interface InjectionOptions {
@@ -18,13 +30,21 @@ export interface InjectionOptions {
1830
/**
1931
* Wraps a user script module for execution in MAIN world
2032
* Converts ES module exports to window.agent.registerTool() calls
33+
* All transformations happen here in the background worker,
34+
* not at runtime in the page.
2135
*/
2236
function wrapScriptForInjection(code: string, metadata: UserScriptMetadata): string {
2337
// Generate a unique script name for debugging (namespace is now required)
2438
const scriptName = `${metadata.namespace}:${metadata.name}`;
39+
const toolName = `${metadata.namespace}_${metadata.name}`;
40+
41+
const transformedCode = code
42+
.replace(/^[\s\n]*'use webmcp-tool v\d+';[\s\n]*/m, '') // Remove pragma
43+
.replace(/export\s+const\s+metadata\s*=/g, 'const metadata =')
44+
.replace(/export\s+(async\s+)?function\s+execute/g, '$1function execute')
45+
.replace(/export\s+function\s+shouldRegister/g, 'function shouldRegister');
2546

26-
// Wrap module code to execute in MAIN world
27-
// Converts ES module exports to tool registration
47+
// Wrap the PRE-TRANSFORMED code for direct execution (no eval/Function needed)
2848
return `
2949
(function() {
3050
'use strict';
@@ -34,59 +54,54 @@ function wrapScriptForInjection(code: string, metadata: UserScriptMetadata): str
3454
console.log('[WebMCP] Script already injected:', scriptId);
3555
return;
3656
}
37-
57+
3858
// Mark as injected
3959
window.__webmcpInjected = window.__webmcpInjected || {};
4060
window.__webmcpInjected[scriptId] = true;
41-
42-
// Execute module code with export interception
61+
62+
console.log('[WebMCP] Executing user script: ${scriptName}');
63+
4364
try {
44-
// Transform the module code to work in a non-module context
45-
const transformedCode = ${JSON.stringify(code)}
46-
.replace(/^\\s*'use webmcp-tool v\\d+';\\s*/, '') // Remove pragma
47-
.replace(/export\\s+const\\s+metadata\\s*=/g, 'const metadata =')
48-
.replace(/export\\s+(async\\s+)?function\\s+execute/g, '$1function execute')
49-
.replace(/export\\s+function\\s+shouldRegister/g, 'function shouldRegister');
50-
51-
// Use Function constructor instead of eval for better security
52-
const moduleFunc = new Function('exports', transformedCode + '; return { metadata, execute, shouldRegister: typeof shouldRegister !== "undefined" ? shouldRegister : undefined };');
53-
const moduleExports = moduleFunc({});
54-
55-
// Check if tool should be registered (optional export)
56-
if (moduleExports.shouldRegister) {
65+
${transformedCode}
66+
67+
console.log('[WebMCP] User script executed, checking exports:', {
68+
hasMetadata: typeof metadata !== 'undefined',
69+
hasExecute: typeof execute !== 'undefined',
70+
hasShouldRegister: typeof shouldRegister !== 'undefined'
71+
});
72+
73+
if (typeof shouldRegister === 'function') {
5774
try {
58-
const shouldReg = moduleExports.shouldRegister();
59-
if (!shouldReg) {
75+
if (!shouldRegister()) {
6076
console.log('[WebMCP] Tool ${scriptName} skipped registration (shouldRegister returned false)');
6177
return;
6278
}
6379
} catch (error) {
64-
log.error('[WebMCP] Error in shouldRegister for ${scriptName}:', error);
80+
console.error('[WebMCP] Error in shouldRegister for ${scriptName}:', error);
6581
// Continue with registration if shouldRegister throws (fail-open)
6682
}
6783
}
68-
69-
// Register tool with window.agent using namespace_name format
70-
if (window.agent && moduleExports.metadata && moduleExports.execute) {
71-
const toolName = moduleExports.metadata.namespace + '_' + moduleExports.metadata.name;
84+
85+
if (window.agent && typeof metadata !== 'undefined' && typeof execute !== 'undefined') {
7286
const tool = {
73-
name: toolName,
74-
description: moduleExports.metadata.description || '${metadata.description || ''}',
75-
inputSchema: moduleExports.metadata.inputSchema,
76-
execute: moduleExports.execute
87+
name: '${toolName}',
88+
description: metadata.description || '${metadata.description || ''}',
89+
inputSchema: metadata.inputSchema || { type: 'object', properties: {} },
90+
execute: execute
7791
};
78-
92+
7993
window.agent.registerTool(tool);
80-
console.log('[WebMCP] Registered tool ' + toolName + ' v${metadata.version}');
94+
console.log('[WebMCP] Registered tool ${toolName} v${metadata.version}');
8195
} else {
82-
log.error('[WebMCP] Failed to register tool ${scriptName}:', {
96+
console.error('[WebMCP] Failed to register ${scriptName}:', {
8397
hasAgent: !!window.agent,
84-
hasMetadata: !!moduleExports.metadata,
85-
hasExecute: !!moduleExports.execute
98+
hasMetadata: typeof metadata !== 'undefined',
99+
hasExecute: typeof execute !== 'undefined'
86100
});
87101
}
102+
88103
} catch (error) {
89-
log.error('[WebMCP] Error executing script ${scriptName}:', error);
104+
console.error('[WebMCP] Error executing script ${scriptName}:', error);
90105
}
91106
})();
92107
//# sourceURL=webmcp-script:${scriptName}.js`;
@@ -156,13 +171,53 @@ async function injectSingleScript(
156171
// Always inject at document_idle for consistent behavior
157172
const injectImmediately = false;
158173

159-
// Create an injection function that Chrome can serialize
160-
// We'll pass the wrapped code as an argument to avoid string replacement issues
161174
const injectionFunc = (codeToInject: string) => {
162-
const script = document.createElement('script');
163-
script.textContent = codeToInject;
164-
(document.head || document.documentElement).appendChild(script);
165-
script.remove();
175+
console.warn('[WebMCP] Creating blob URL for user script injection');
176+
177+
try {
178+
// Create a Blob with the script code
179+
const blob = new Blob([codeToInject], { type: 'application/javascript' });
180+
const blobUrl = URL.createObjectURL(blob);
181+
182+
console.warn('[WebMCP] Blob URL created:', blobUrl);
183+
184+
// Load script from blob: URL (external source, not inline)
185+
const script = document.createElement('script');
186+
187+
// Try to set src - may need Trusted Types policy on strict sites
188+
try {
189+
// Use TT policy if available (created by webmcp-polyfill.js)
190+
if (window.__agentboardTTPolicy) {
191+
console.warn('[WebMCP] Using Trusted Types policy for user script');
192+
script.src = window.__agentboardTTPolicy.createScriptURL(blobUrl);
193+
} else {
194+
script.src = blobUrl;
195+
}
196+
} catch (trustedTypesError) {
197+
console.error('[WebMCP] ❌ Trusted Types blocked user script injection');
198+
console.error('[WebMCP] This site requires TrustedScriptURL but policy creation failed');
199+
console.error('[WebMCP] Possible reasons:');
200+
console.error('[WebMCP] 1. CSP restricts policy names (trusted-types directive)');
201+
console.error('[WebMCP] 2. Site blocks all dynamic policy creation');
202+
console.error('[WebMCP] Technical details:', trustedTypesError);
203+
204+
URL.revokeObjectURL(blobUrl);
205+
return;
206+
}
207+
208+
script.onload = () => {
209+
console.warn('[WebMCP] ✅ User script loaded successfully via blob URL');
210+
URL.revokeObjectURL(blobUrl);
211+
};
212+
script.onerror = (e) => {
213+
console.error('[WebMCP] ❌ Failed to load script from blob URL:', e);
214+
URL.revokeObjectURL(blobUrl);
215+
};
216+
217+
(document.head || document.documentElement).appendChild(script);
218+
} catch (error) {
219+
console.error('[WebMCP] ❌ Unexpected error during blob injection:', error);
220+
}
166221
};
167222

168223
// Inject the script

0 commit comments

Comments
 (0)