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>

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();
}