Skip to content

fix(examples): resolve CSP 'unsafe-eval' violation in threejs-server#432

Open
Robloncz wants to merge 1 commit intomodelcontextprotocol:mainfrom
Robloncz:fix/threejs-csp-violation
Open

fix(examples): resolve CSP 'unsafe-eval' violation in threejs-server#432
Robloncz wants to merge 1 commit intomodelcontextprotocol:mainfrom
Robloncz:fix/threejs-csp-violation

Conversation

@Robloncz
Copy link

@Robloncz Robloncz commented Feb 5, 2026

Summary of changes

Replaced new Function() with a dynamic <script> tag execution pattern in the threejs-server example's executeThreeCode function.

Motivation and Context

Strict Content Security Policies (CSP) often block unsafe-eval, which prevents the use of new Function(). This caused the Three.js example to fail in environments like the VS Code MCP App host with the error:
"Evaluating a string as JavaScript violates the following Content Security Policy directive...".

By switching to dynamic script injection and a temporary global context object, the app can execute LLM-generated 3D code while adhering to unsafe-inline policies, significantly improving compatibility with secure hosts.

How Has This Been Tested?

  • Verified rendering of a 3D rotating cube in a CSP-restricted host.
  • Confirmed that the unsafe-eval error is no longer triggered.
  • Successful build via npm run build in examples/threejs-server.
  • Code style verified with npm run prettier:fix.

Breaking Changes

None. This is an internal implementation change for the example renderer.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The implementation uses a unique window-level ID (threejs_ctx_...) to pass the execution context (THREE, canvas, etc.) to the dynamic script without collisions. The global reference is deleted immediately after execution to keep the environment clean.

Copilot AI review requested due to automatic review settings February 5, 2026 09:46
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a Content Security Policy (CSP) violation in the threejs-server example by replacing new Function() with dynamic script injection to avoid unsafe-eval restrictions.

Changes:

  • Modified executeThreeCode function to use a temporary global context object and dynamic <script> tag injection instead of new Function()
  • Added error handling with try-catch-finally blocks around the injected code execution
  • Implemented automatic cleanup of temporary global context after code execution

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +181 to +212
// Use a unique ID to avoid conflicts
const scriptId = `threejs_ctx_${Math.random().toString(36).slice(2, 11)}`;

// Expose context to window temporarily so the script tag can access it
(window as any)[scriptId] = {
threeContext,
canvas,
width,
height,
visibilityAwareRAF,
};

const wrappedCode = `
(async () => {
const { threeContext, canvas, width, height, visibilityAwareRAF } = window['${scriptId}'];
const { THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass } = threeContext;
const requestAnimationFrame = visibilityAwareRAF;

try {
${code}
} catch (e) {
console.error('Three.js execution error:', e);
} finally {
delete window['${scriptId}'];
}
})();
`;

const script = document.createElement("script");
script.textContent = wrappedCode;
document.head.appendChild(script);
script.remove();
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The async execution is no longer awaited. The original code used await fn(...) which ensured the async function completed before executeThreeCode returned. With the new script injection approach, the function returns immediately after appending the script, not after the async IIFE inside completes. This breaks the promise chain and means errors from the Three.js code execution won't be caught by the .catch() handler at line 278. Consider using a Promise-based callback mechanism where the injected script resolves/rejects a promise that executeThreeCode returns.

Suggested change
// Use a unique ID to avoid conflicts
const scriptId = `threejs_ctx_${Math.random().toString(36).slice(2, 11)}`;
// Expose context to window temporarily so the script tag can access it
(window as any)[scriptId] = {
threeContext,
canvas,
width,
height,
visibilityAwareRAF,
};
const wrappedCode = `
(async () => {
const { threeContext, canvas, width, height, visibilityAwareRAF } = window['${scriptId}'];
const { THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass } = threeContext;
const requestAnimationFrame = visibilityAwareRAF;
try {
${code}
} catch (e) {
console.error('Three.js execution error:', e);
} finally {
delete window['${scriptId}'];
}
})();
`;
const script = document.createElement("script");
script.textContent = wrappedCode;
document.head.appendChild(script);
script.remove();
return new Promise<void>((resolve, reject) => {
// Use a unique ID to avoid conflicts
const scriptId = `threejs_ctx_${Math.random().toString(36).slice(2, 11)}`;
// Expose context to window temporarily so the script tag can access it
(window as any)[scriptId] = {
threeContext,
canvas,
width,
height,
visibilityAwareRAF,
resolve,
reject,
};
const wrappedCode = `
(async () => {
const { threeContext, canvas, width, height, visibilityAwareRAF, resolve, reject } = window['${scriptId}'];
const { THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass } = threeContext;
const requestAnimationFrame = visibilityAwareRAF;
try {
${code}
resolve();
} catch (e) {
console.error('Three.js execution error:', e);
reject(e);
} finally {
delete window['${scriptId}'];
}
})();
`;
const script = document.createElement("script");
script.textContent = wrappedCode;
document.head.appendChild(script);
script.remove();
});

Copilot uses AI. Check for mistakes.
Comment on lines +193 to +207
const wrappedCode = `
(async () => {
const { threeContext, canvas, width, height, visibilityAwareRAF } = window['${scriptId}'];
const { THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass } = threeContext;
const requestAnimationFrame = visibilityAwareRAF;

try {
${code}
} catch (e) {
console.error('Three.js execution error:', e);
} finally {
delete window['${scriptId}'];
}
})();
`;
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct string interpolation of user code creates a potential code injection vulnerability. If the code parameter contains backticks or template literal expressions like ${...}, it could break out of the template string context or interfere with the wrapper logic. Since this code is meant to execute LLM-generated content, there's a real risk. Consider using string concatenation instead of template literals for the wrapper, or properly escape the code before interpolation.

Suggested change
const wrappedCode = `
(async () => {
const { threeContext, canvas, width, height, visibilityAwareRAF } = window['${scriptId}'];
const { THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass } = threeContext;
const requestAnimationFrame = visibilityAwareRAF;
try {
${code}
} catch (e) {
console.error('Three.js execution error:', e);
} finally {
delete window['${scriptId}'];
}
})();
`;
const wrappedCode =
"(async () => {\n" +
" const { threeContext, canvas, width, height, visibilityAwareRAF } = window['" + scriptId + "'];\n" +
" const { THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass } = threeContext;\n" +
" const requestAnimationFrame = visibilityAwareRAF;\n" +
" try {\n" +
code +
"\n" +
" } catch (e) {\n" +
" console.error('Three.js execution error:', e);\n" +
" } finally {\n" +
" delete window['" + scriptId + "'];\n" +
" }\n" +
"})();\n";

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants