24Advanced
Browser APIs
Browser APIs Security — Instruction 24
Coverage
WebRTC, WebAssembly (WASM), PWA/Service Workers, IndexedDB, Web Workers, Push Notifications Modern browser API security
WebRTC Security
1. IP Leak via STUN
// 🔴 STUN servers expose real IP even through VPN
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.googleapis.com:19302' }]
})
// Creates ICE candidates with real local + public IP
// 🟢 Force TURN relay to hide IPs
const pc = new RTCPeerConnection({
iceServers: [{
urls: 'turn:turn.yourdomain.com:3478',
username: process.env.TURN_USER,
credential: process.env.TURN_PASS
}],
iceTransportPolicy: 'relay' // force all traffic through TURN
})
// 🔴 STUN/TURN credentials in frontend code
const turnUser = 'hardcodedUser' // 🔴
// 🟢 Short-lived TURN credentials from server
const { username, password } = await fetch('/api/turn-credentials').then(r => r.json())
// Server generates time-limited TURN credentials
2. TURN Server Authentication
// 🔴 Open TURN server (no auth) = free relay for anyone
// Massive bandwidth costs + abuse
// 🟢 Time-limited HMAC credentials
// Server:
function generateTurnCredentials(userId) {
const ttl = 86400 // 24 hours
const timestamp = Math.floor(Date.now() / 1000) + ttl
const username = `${timestamp}:${userId}`
const password = crypto.createHmac('sha1', process.env.TURN_SECRET).update(username).digest('base64')
return { username, password, ttl }
}
3. WebRTC Data Channel Security
// 🔴 Sending sensitive data over data channels without application-level encryption
// WebRTC uses DTLS — encrypted in transit, but verify peer
// 🟢 Verify peer certificate fingerprint
pc.addEventListener('connectionstatechange', () => {
if (pc.connectionState === 'connected') {
const certStats = pc.getStats().then(stats => {
// Verify peer fingerprint against expected
})
}
})
WebAssembly (WASM)
4. Load WASM from Trusted Sources Only
// 🔴 Loading WASM from user-controlled URL
const module = await WebAssembly.instantiateStreaming(fetch(userUrl))
// 🟢 WASM from fixed, trusted sources only
const module = await WebAssembly.instantiateStreaming(fetch('/static/app.wasm'))
// 🟢 CSP: control which WASM can be loaded
// Add to CSP if using WASM:
// script-src 'self' 'wasm-unsafe-eval'
5. Validate Inputs at JS/WASM Boundary
// 🔴 Unvalidated input passed to WASM (may have internal buffer issues)
wasmModule.exports.processData(userInput) // WASM compiled from C may overflow
// 🟢 Validate length and type before passing to WASM
if (typeof userInput !== 'string' || userInput.length > MAX_INPUT_LENGTH) {
throw new Error('Invalid input')
}
wasmModule.exports.processData(userInput)
Service Workers
6. Minimal Service Worker Scope
// 🔴 Service worker registered at root with broad scope
navigator.serviceWorker.register('/sw.js', { scope: '/' })
// SW intercepts ALL requests on the domain
// 🟢 Scope to specific path
navigator.serviceWorker.register('/app/sw.js', { scope: '/app/' })
7. Never Cache Auth/Sensitive Requests
// sw.js
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)
// 🔴 Caching auth requests
// 🟢 Never cache:
if (url.pathname.startsWith('/api/auth') ||
url.pathname.startsWith('/api/user') ||
event.request.headers.has('Authorization')) {
// Always fetch fresh from network
event.respondWith(fetch(event.request))
return
}
// Cache only static assets
if (url.pathname.match(/\.(js|css|png|jpg|svg|woff2)$/)) {
event.respondWith(cacheFirst(event.request))
}
})
8. SW Update Lifecycle
// 🟢 Force update check to ensure security patches are applied
self.addEventListener('install', (event) => {
self.skipWaiting() // activate immediately
})
self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim()) // take control immediately
})
IndexedDB Security
9. Encrypt Sensitive Data Before Storage
// 🔴 Sensitive data in plaintext in IndexedDB
const db = await openDB('appDB', 1)
await db.put('users', { token: authToken, email: user.email }, 'current')
// Accessible via: dev tools, XSS, extensions
// 🟢 Encrypt before storing
const encrypted = await encryptWithWebCrypto(JSON.stringify({ token: authToken }), encryptionKey)
await db.put('secure', { data: encrypted }, 'current')
// 🟢 Or use SecureStorage alternatives
// For truly sensitive data: httpOnly cookies (not accessible to JS at all)
10. Clear IndexedDB on Logout
// 🔴 Sensitive data persists after logout
// 🟢 Clear all sensitive data on logout
async function logout() {
// Clear auth state
await clearAuthCookies()
// Clear indexed DB
const db = await openDB('appDB')
await db.clear('secure')
await db.clear('users')
// Clear storage
localStorage.clear()
sessionStorage.clear()
}
Web Workers
11. No Sensitive Data in Worker Messages
// 🔴 Passing API keys to workers
const worker = new Worker('/worker.js')
worker.postMessage({ apiKey: process.env.API_KEY }) // key accessible to worker
// 🟢 Workers should only receive data needed for computation
worker.postMessage({ data: arrayBuffer, config: { quality: 80 } })
12. Worker Source Validation
// 🔴 Creating worker from user-controlled URL
const worker = new Worker(req.query.workerUrl) // code injection!
// 🟢 Workers from known, static paths only
const worker = new Worker('/static/workers/image-processor.js')
Push Notifications (VAPID)
13. VAPID Key Security
// 🔴 VAPID private key in frontend code
const vapidPrivateKey = 'xxxPrivateKeyHere' // 🔴 NEVER in frontend
// 🟢 VAPID key split:
// Frontend: only vapidPublicKey (safe to expose)
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY)
})
// Backend: uses vapidPrivateKey to sign notifications
webpush.setVapidDetails('mailto:you@example.com', publicKey, process.env.VAPID_PRIVATE_KEY)
14. Push Subscription Endpoint Auth
// 🔴 Push subscription endpoint not authenticated
app.post('/api/push/subscribe', (req, res) => {
saveSubscription(req.body) // no auth!
// 🟢 Require auth to save subscription
app.post('/api/push/subscribe', requireAuth, (req, res) => {
saveSubscription(req.body, req.user.id)
})
PWA Manifest Security
15. Manifest Security
// manifest.json
{
"name": "My App",
"start_url": "/",
"scope": "/app/", // 🟢 Restrict scope — don't control more than needed
// 🔴 Icons from external CDN without SRI
// "icons": [{ "src": "https://external.com/icon.png" }]
// 🟢 Icons from same origin
"icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }],
// 🟢 Display standalone — OK
"display": "standalone"
}