WebAssembly provides strong security guarantees through sandboxing, but proper implementation is crucial for secure applications.
WASM Security Model
Sandboxing
WASM modules run in a sandboxed execution environment:
- No direct access to host system
- Explicit imports for external functionality
- Memory isolation from host
- Controlled resource limits
wat
(module
;; Memory is isolated from host
(memory (export "memory") 1)
;; Can only call explicitly imported functions
(import "env" "log" (func $log (param i32)))
(func (export "add") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add
)
)Security Best Practices
1. Validate Imports
typescript
// WASM security wrapper
class SecureWASMRuntime {
private allowedImports: Set<string>;
constructor(allowedImports: string[]) {
this.allowedImports = new Set(allowedImports);
}
async loadModule(wasmBytes: Buffer, imports: Record<string, any>) {
// Validate all imports
for (const [module, functions] of Object.entries(imports)) {
for (const func of Object.keys(functions)) {
const key = `${module}.${func}`;
if (!this.allowedImports.has(key)) {
throw new Error(`Unauthorized import: ${key}`);
}
}
}
// Load with validated imports
const module = await WebAssembly.instantiate(wasmBytes, imports);
return module.instance;
}
}
// Usage
const runtime = new SecureWASMRuntime([
'env.log',
'env.memory',
'math.random'
]);
const instance = await runtime.loadModule(wasmCode, {
env: {
log: console.log,
memory: new WebAssembly.Memory({ initial: 1 })
},
math: {
random: Math.random
}
});2. Resource Limits
typescript
// Resource-constrained WASM execution
class ConstrainedWASMRuntime {
private maxExecutionTime: number;
private maxMemory: number;
constructor(maxExecutionTime: number, maxMemory: number) {
this.maxExecutionTime = maxExecutionTime;
this.maxMemory = maxMemory;
}
async executeWithTimeout(instance: WebAssembly.Instance, func: string, args: any[]) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Execution timeout')), this.maxExecutionTime);
});
const execution = this.execute(instance, func, args);
try {
return await Promise.race([timeout, execution]);
} catch (error) {
if (error.message === 'Execution timeout') {
// Terminate execution
throw new Error('WASM execution exceeded time limit');
}
throw error;
}
}
private async execute(instance: WebAssembly.Instance, func: string, args: any[]) {
const exports = instance.exports as any;
const funcToCall = exports[func];
if (typeof funcToCall !== 'function') {
throw new Error(`Export ${func} is not a function`);
}
return funcToCall(...args);
}
}3. Memory Safety
typescript
// Safe memory access wrapper
class SafeWASMMemory {
private memory: WebAssembly.Memory;
private size: number;
constructor(memory: WebAssembly.Memory) {
this.memory = memory;
this.size = memory.buffer.byteLength;
}
readString(offset: number, length: number): string {
this.validateAccess(offset, length);
const bytes = new Uint8Array(this.memory.buffer, offset, length);
return new TextDecoder().decode(bytes);
}
writeString(offset: number, str: string): number {
const bytes = new TextEncoder().encode(str);
this.validateAccess(offset, bytes.length);
new Uint8Array(this.memory.buffer, offset, bytes.length).set(bytes);
return bytes.length;
}
private validateAccess(offset: number, length: number) {
if (offset < 0 || offset + length > this.size) {
throw new Error('Memory access out of bounds');
}
}
}Input Validation
typescript
// Validate WASM module before execution
async function validateWASMModule(wasmBytes: Buffer): Promise<boolean> {
try {
// Check for valid WASM header
if (wasmBytes[0] !== 0x00 || wasmBytes[1] !== 0x61 ||
wasmBytes[2] !== 0x73 || wasmBytes[3] !== 0x6D) {
return false;
}
// Attempt to compile
await WebAssembly.compile(wasmBytes);
// Additional checks
const module = await WebAssembly.instantiate(wasmBytes);
const exports = Object.keys(module.instance.exports);
// Check for dangerous exports
const dangerous = exports.filter(e =>
e.includes('grow') ||
e.includes('table') ||
e.includes('memory')
);
return dangerous.length === 0;
} catch {
return false;
}
}Secure Communication
typescript
// Secure host-WASM communication
class SecureHostInterface {
private channel: MessageChannel;
constructor(wasmWorker: Worker) {
this.channel = new MessageChannel();
// Setup secure messaging
wasmWorker.postMessage({
type: 'init',
port: this.channel.port2
}, [this.channel.port2]);
// Listen for messages
this.channel.port1.onmessage = (event) => {
this.handleWASMMessage(event.data);
};
}
private async handleWASMMessage(message: any) {
// Validate message structure
if (!message.type || typeof message.type !== 'string') {
console.error('Invalid message from WASM');
return;
}
// Route message based on type
switch (message.type) {
case 'fetch':
await this.handleSecureFetch(message.url);
break;
case 'log':
console.log('[WASM]', message.data);
break;
default:
console.warn('Unknown message type:', message.type);
}
}
private async handleSecureFetch(url: string) {
// Validate URL
const parsed = new URL(url);
if (!['https:', 'http:'].includes(parsed.protocol)) {
throw new Error('Unsafe protocol');
}
if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
throw new Error('Host not allowed');
}
// Fetch with security headers
const response = await fetch(url, {
headers: {
'X-WASM-Request': 'true'
}
});
return response.json();
}
}Conclusion
WASM provides strong security foundations, but proper implementation is essential. Validate all inputs, limit resources, and monitor execution carefully.