Securely Initialize Ammo.js in an Iframe
Integrating the Ammo.js physics engine inside an iframe requires careful handling of its asynchronous WebAssembly (Wasm) loading process to prevent race conditions and security vulnerabilities. This article explains how to securely sandbox the iframe, safely initialize Ammo.js, and establish secure cross-origin communication between the parent window and the iframe using the HTML5 PostMessage API.
1. Configure the Iframe Sandbox and CSP
To secure the execution environment, the host document must restrict
the iframe’s capabilities using the sandbox attribute.
Because Ammo.js requires WebAssembly, you must allow scripts to run.
<iframe
src="physics-worker.html"
sandbox="allow-scripts"
csp="script-src 'self' 'wasm-unsafe-eval'; worker-src 'self';"
id="physics-iframe">
</iframe>allow-scripts: Allows the iframe to execute JavaScript and load the Ammo.js WebAssembly binary.wasm-unsafe-eval: Required in modern Content Security Policies (CSP) to allow the compilation and execution of WebAssembly modules like Ammo.js.
2. Implement Asynchronous Ammo.js Initialization
Inside the iframe (physics-worker.html), Ammo.js must be
initialized asynchronously. The modern builds of Ammo.js return a
Promise-like object when called. Wrap this initialization in a
module-level load state to ensure the engine is fully ready before
accepting any simulation requests.
// physics-worker.js
let physicsWorld = null;
let isAmmoReady = false;
// Initialize Ammo.js safely
Ammo().then((AmmoLib) => {
Ammo = AmmoLib; // Re-assign global Ammo reference
initializePhysicsEngine();
isAmmoReady = true;
// Notify the parent window that initialization was successful
notifyParent({ status: 'ready' });
}).catch((error) => {
console.error("Failed to initialize Ammo.js:", error);
notifyParent({ status: 'error', message: error.message });
});
function initializePhysicsEngine() {
const collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
const dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
const overlappingPairCache = new Ammo.btDbvtBroadphase();
const solver = new Ammo.btSequentialImpulseConstraintSolver();
physicsWorld = new Ammo.btDiscreteDynamicsWorld(
dispatcher,
overlappingPairCache,
solver,
collisionConfiguration
);
physicsWorld.setGravity(new Ammo.btVector3(0, -9.81, 0));
}3. Establish Secure Cross-Origin Communication
Because the sandboxed iframe acts as an isolated context, you must
use postMessage to send commands from the parent window.
Security must be enforced on both ends by validating message
origins.
Inside the Iframe (Sending Ready Signal)
To prevent leaking sensitive state information, specify the exact origin of the parent window when sending the success signal.
function notifyParent(message) {
// Replace with your parent window's strict origin
const targetOrigin = "https://your-main-app.com";
window.parent.postMessage(message, targetOrigin);
}In the Parent Window (Receiving the Signal)
The parent window must listen for the initialization success message before dispatching physics commands to the iframe. It must strictly validate the origin of the sender.
const ALLOWED_IFRAME_ORIGIN = "https://your-iframe-domain.com";
const iframe = document.getElementById('physics-iframe');
window.addEventListener('message', (event) => {
// Reject messages from unauthorized origins
if (event.origin !== ALLOWED_IFRAME_ORIGIN) {
return;
}
const { status, message } = event.data;
if (status === 'ready') {
console.log("Ammo.js loaded securely inside the iframe. Starting simulation.");
startSimulation();
} else if (status === 'error') {
console.error("Iframe physics error:", message);
}
});
function startSimulation() {
// Safely send data to the iframe
iframe.contentWindow.postMessage({ command: 'start' }, ALLOWED_IFRAME_ORIGIN);
}4. Handle Incoming Simulation Messages Securely
Inside the iframe, set up an event listener to handle commands from the parent window. Ensure you check the origin and verify that Ammo.js is fully initialized before executing any physics calculations to prevent null pointer exceptions in the WebAssembly space.
window.addEventListener('message', (event) => {
const expectedParentOrigin = "https://your-main-app.com";
if (event.origin !== expectedParentOrigin) {
return; // Silent rejection of untrusted sources
}
if (!isAmmoReady) {
console.warn("Received message before Ammo.js was ready.");
return;
}
const { command, data } = event.data;
if (command === 'start') {
runPhysicsLoop();
}
});
function runPhysicsLoop() {
// Step the physics world inside requestAnimationFrame or a set interval
const deltaTime = 1 / 60;
physicsWorld.stepSimulation(deltaTime, 10);
// Extract transform data and send it back to the parent
sendTransformDataBack();
}