Skip to content
All essays
WebMarch 27, 202612 min

WebAssembly Security: Safe WASM Development

Master WebAssembly security and build safe, secure WASM applications

Ü
Ümit Uz
Mobile & Full Stack Developer

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.

Enjoyed this essay? Share it.

Related essays

Next essay
WebAssembly Performance: Optimization Techniques