This guide covers security best practices, threat models, and recommended configurations for using PAN in production environments.
PAN assumes all code running in the same JavaScript context is trusted. PAN provides:
PAN does NOT provide:
┌─────────────────────────────────────┐
│ Same-Origin Context (TRUSTED) │
│ ┌────────────┐ ┌────────────┐ │
│ │ pan-bus │ │ Component │ │
│ └────────────┘ └────────────┘ │
│ ↕ ↕ │
│ CustomEvents (trusted) │
└─────────────────────────────────────┘
↕ postMessage
┌─────────────────────────────────────┐
│ Cross-Origin iframe (UNTRUSTED) │
│ Requires pan-gateway with │
│ topic allowlisting │
└─────────────────────────────────────┘
Without limits, retained messages can grow indefinitely:
// BAD: Can exhaust memory
for (let i = 0; i < 1000000; i++) {
client.publish({
topic: `sensor.${i}`,
data: { value: Math.random() },
retain: true // Each creates a new retained message!
});
}
<!-- Configure memory limits -->
<pan-bus-enhanced
max-retained="1000"
max-message-size="1048576"
debug="true">
</pan-bus-enhanced>
// Messages beyond limit are evicted (LRU)
for (let i = 0; i < 2000; i++) {
client.publish({
topic: `sensor.${i}`,
data: { value: Math.random() },
retain: true
});
}
// Only 1000 most recent are kept
// Request bus statistics
client.publish({ topic: 'pan:sys.stats', data: {} });
client.subscribe('pan:sys.stats', (msg) => {
console.log('Bus stats:', msg.data);
// {
// published: 12543,
// delivered: 50172,
// retained: 850,
// retainedEvicted: 150,
// subscriptions: 23
// }
});
PAN automatically validates that message data is JSON-serializable:
// ✅ GOOD: These work
client.publish({ topic: 'test', data: { name: 'Alice' } });
client.publish({ topic: 'test', data: [1, 2, 3] });
client.publish({ topic: 'test', data: 'string' });
client.publish({ topic: 'test', data: 42 });
client.publish({ topic: 'test', data: null });
// ❌ BAD: These will be rejected
client.publish({ topic: 'test', data: document.body }); // DOM node
client.publish({ topic: 'test', data: () => {} }); // Function
client.publish({ topic: 'test', data: new Map() }); // Not serializable
// Handle circular references
const obj = { name: 'Alice' };
obj.self = obj; // Circular!
client.publish({ topic: 'test', data: obj }); // ❌ Rejected
// Listen for validation errors
client.subscribe('pan:sys.error', (msg) => {
console.error('PAN Error:', msg.data);
// {
// code: 'MESSAGE_INVALID',
// message: 'Circular references are not allowed',
// details: { topic: 'test' }
// }
});
// Configure size limits
<pan-bus-enhanced
max-message-size="1048576" <!-- 1MB total -->
max-payload-size="524288"> <!-- 512KB data -->
</pan-bus-enhanced>
// Large messages are rejected
const hugeData = new Array(1000000).fill('x').join('');
client.publish({
topic: 'test',
data: { huge: hugeData } // ❌ Rejected if > 512KB
});
For maximum security, use strict CSP headers:
<!-- Strict CSP (recommended for production) -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
">
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self';
">
</head>
<body>
<!-- ✅ Works with CSP -->
<script type="module" src="/pan/core/pan-bus-enhanced.mjs"></script>
<script type="module" src="/pan/core/pan-client.mjs"></script>
<pan-bus-enhanced></pan-bus-enhanced>
<script type="module">
import { PanClient } from '/pan/core/pan-client.mjs';
const client = new PanClient();
// ... your code
</script>
</body>
</html>
// ❌ BAD: Inline event handlers violate CSP
<button onclick="handleClick()">Click</button>
// ✅ GOOD: Use addEventListener
<button id="myButton">Click</button>
<script type="module">
document.getElementById('myButton')
.addEventListener('click', handleClick);
</script>
// ❌ BAD: eval() violates CSP
eval('alert("test")');
// ✅ GOOD: Don't use eval, use structured data
client.publish({ topic: 'action', data: { type: 'alert', msg: 'test' } });
Global wildcard subscriptions can expose sensitive data:
// ⚠️ DANGEROUS: Sees ALL messages on the bus
client.subscribe('*', (msg) => {
console.log('Intercepted:', msg.topic, msg.data);
// Could capture passwords, tokens, PII, etc.
});
<!-- Disable global wildcard in production -->
<pan-bus-enhanced allow-global-wildcard="false"></pan-bus-enhanced>
// Now this will be rejected
client.subscribe('*', handler);
// Error: Global wildcard (*) is disabled for security
// ✅ GOOD: Scoped wildcards are fine
client.subscribe('users.*', handler); // Only user topics
client.subscribe('app.settings.*', handler); // Only app settings
Use topic prefixes to isolate sensitive data:
// Public topics (okay to wildcard)
'public.news.*'
'public.weather.*'
// Private topics (no wildcards)
'private.user.session'
'private.auth.token'
// Internal topics (restricted)
'internal.admin.*'
Without rate limiting, malicious or buggy code can flood the bus:
// BAD: Can DoS the bus
setInterval(() => {
for (let i = 0; i < 1000; i++) {
client.publish({ topic: 'spam', data: i });
}
}, 10); // 100,000 messages per second!
<pan-bus-enhanced
rate-limit="1000"
rate-limit-window="1000">
</pan-bus-enhanced>
// Client is limited to 1000 messages per second
// Excess messages are dropped and error is emitted
client.subscribe('pan:sys.error', (msg) => {
if (msg.data.code === 'RATE_LIMIT_EXCEEDED') {
console.warn('Rate limit hit:', msg.data.details);
}
});
// Development
<pan-bus-enhanced rate-limit="10000"></pan-bus-enhanced> // Permissive
// Staging
<pan-bus-enhanced rate-limit="1000"></pan-bus-enhanced> // Moderate
// Production
<pan-bus-enhanced rate-limit="500"></pan-bus-enhanced> // Conservative
// ❌ DANGEROUS: XSS vulnerability
client.subscribe('chat.message', (msg) => {
document.body.innerHTML += msg.data.html; // XSS!
});
// ✅ SAFE: Use textContent
client.subscribe('chat.message', (msg) => {
const p = document.createElement('p');
p.textContent = msg.data.text; // Escaped automatically
document.body.appendChild(p);
});
If using pan-markdown-renderer, always sanitize:
// ⚠️ Components like pan-markdown-renderer need audit
<pan-markdown-renderer sanitize="true"></pan-markdown-renderer>
// ❌ DANGEROUS: javascript: URLs
client.subscribe('link.clicked', (msg) => {
window.location = msg.data.url; // Can be "javascript:alert(1)"
});
// ✅ SAFE: Validate protocol
client.subscribe('link.clicked', (msg) => {
const url = new URL(msg.data.url);
if (url.protocol === 'http:' || url.protocol === 'https:') {
window.location = url.href;
}
});
These components handle user input and need security review:
// ✅ Use with caution in production
// 1. Review component source
// 2. Test with malicious input
// 3. Use in sandboxed context if possible
// 4. Monitor for suspicious activity
// Example: Sandboxed markdown renderer
<iframe sandbox="allow-same-origin" srcdoc="
<pan-markdown-renderer></pan-markdown-renderer>
"></iframe>
allow-global-wildcard="false")pan:sys.error events<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Production App</title>
<!-- Strict CSP -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
connect-src 'self' https://api.example.com;
">
</head>
<body>
<!-- Secure bus configuration -->
<pan-bus-enhanced
max-retained="500"
max-message-size="524288"
rate-limit="500"
allow-global-wildcard="false"
debug="false">
</pan-bus-enhanced>
<script type="module">
import { PanClient } from '/pan/core/pan-client.mjs';
const client = new PanClient();
// Error monitoring
client.subscribe('pan:sys.error', (msg) => {
// Log to monitoring service
console.error('PAN Error:', msg.data);
sendToMonitoring(msg.data);
});
// Periodically check bus health
setInterval(() => {
client.publish({ topic: 'pan:sys.stats', data: {} });
}, 60000);
client.subscribe('pan:sys.stats', (msg) => {
const { retained, subscriptions, errors } = msg.data;
if (errors > 100) {
console.warn('High error rate detected');
}
if (retained > 400) {
console.warn('Approaching retained message limit');
}
});
</script>
</body>
</html>
// Prometheus-style metrics
client.subscribe('pan:sys.stats', (msg) => {
const metrics = {
'pan_messages_published_total': msg.data.published,
'pan_messages_delivered_total': msg.data.delivered,
'pan_messages_dropped_total': msg.data.dropped,
'pan_retained_messages': msg.data.retained,
'pan_active_subscriptions': msg.data.subscriptions,
'pan_errors_total': msg.data.errors
};
// Export to monitoring system
pushMetrics(metrics);
});
// Detect suspicious patterns
client.subscribe('*', (msg) => {
// Check for suspicious topics
if (msg.topic.includes('..') || msg.topic.includes('admin')) {
logSecurityEvent('SUSPICIOUS_TOPIC', msg);
}
// Check for large payloads
if (JSON.stringify(msg.data).length > 100000) {
logSecurityEvent('LARGE_PAYLOAD', msg);
}
// Check for high frequency from one source
trackMessageRate(msg.clientId);
});
// Clear all retained messages
client.publish({ topic: 'pan:sys.clear-retained', data: {} });
// Or clear specific pattern
client.publish({
topic: 'pan:sys.clear-retained',
data: { pattern: 'compromised.*' }
});
DO NOT file public issues for security vulnerabilities.
Email: security@example.com (encrypted preferred)
PGP Key: [link to public key]
We aim to respond within 48 hours.