mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
Generated by Spark: Improve security, sandbox as much stuff as possible and scan and filter malicious code - probably warn user if some virus code has been inputted.
This commit is contained in:
26
PRD.md
26
PRD.md
@@ -234,6 +234,24 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
|
||||
- Includes code examples where relevant
|
||||
- Provides best practices and tips
|
||||
|
||||
### 8. Security Scanning & Sandboxing
|
||||
**Functionality:** Comprehensive code security analysis with sandboxed execution for Lua scripts
|
||||
**Purpose:** Protect against malicious code, XSS attacks, SQL injection, and other vulnerabilities
|
||||
**Trigger:** Automatic scan on save/execute, manual scan via Security Scan button
|
||||
**Progression:** User writes code → Clicks save/execute → System scans for security issues → If critical/high severity detected → Show security warning dialog → Display all issues with details → User reviews and either fixes code or force-proceeds (non-critical only) → System logs security events
|
||||
**Success Criteria:**
|
||||
- All JavaScript code scanned for: eval(), innerHTML, XSS patterns, prototype pollution
|
||||
- All Lua code scanned for: os/io module usage, file loading, infinite loops, global manipulation
|
||||
- All JSON scanned for: __proto__ injection, script tags, malformed data
|
||||
- Critical severity blocks execution/saving completely
|
||||
- High severity requires user acknowledgment to proceed
|
||||
- Medium/Low severity shows warnings but allows operation
|
||||
- Each issue shows: type, severity, message, line number, code pattern, recommendation
|
||||
- Lua scripts execute in sandbox with: disabled os/io/debug modules, 5s timeout, restricted globals
|
||||
- Security scan button available in: Lua Editor, Code Editor, JSON Editor
|
||||
- Security dialog shows color-coded severity levels with icons
|
||||
- Sandboxed Lua engine blocks file system, OS commands, and package loading
|
||||
|
||||
## Edge Case Handling
|
||||
- **Multiple supergod attempts** - Database constraint ensures only one supergod role exists; attempting to create second fails
|
||||
- **Power transfer to self** - UI prevents selecting current supergod user as transfer target
|
||||
@@ -257,10 +275,18 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
|
||||
- **Empty dropdown options** - Validation prevents saving dropdowns with zero options
|
||||
- **Duplicate class selection** - System prevents selecting same class twice
|
||||
- **Import/export conflicts** - Monaco editor validates JSON before import, shows detailed errors
|
||||
- **Malicious code injection** - Security scanner blocks critical threats, warns on suspicious patterns
|
||||
- **XSS attacks via innerHTML** - Scanner detects and prevents dangerous HTML injection patterns
|
||||
- **Lua sandbox escape attempts** - Sandboxed engine disables os/io modules and dangerous functions
|
||||
- **Infinite loops in Lua** - Execution timeout (5s) prevents resource exhaustion
|
||||
- **SQL injection in strings** - Pattern matching detects and warns about SQL injection attempts
|
||||
- **Prototype pollution** - Scanner detects __proto__ manipulation in JavaScript and JSON
|
||||
|
||||
## Design Direction
|
||||
The Level 5 interface should feel like a command center with regal, powerful aesthetics distinct from the purple god-tier panel. Use amber/gold accents to signify supreme authority. The multi-tenant view uses card-based layouts with organizational emphasis. Power transfer UI employs serious warning states with amber colors to communicate irreversibility. The interface balances grandeur with usability—never sacrificing clarity for visual flair. Color hierarchy: amber for supergod actions, purple for god-level previews, standard accent colors for tenant management.
|
||||
|
||||
**Security UX:** Security warnings use shield icons and color-coded severity badges. Critical issues display prominent red warnings with block actions. The security scan dialog provides educational content explaining each issue with recommendations. Warnings are never dismissive—they empower users to write better, safer code.
|
||||
|
||||
## Color Selection
|
||||
|
||||
**Primary Color:** `oklch(0.55 0.18 290)` - Purple/magenta representing creativity and visual design (Levels 1-4)
|
||||
|
||||
267
SECURITY_GUIDE.md
Normal file
267
SECURITY_GUIDE.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# MetaBuilder Security Guide
|
||||
|
||||
## Overview
|
||||
|
||||
MetaBuilder now includes comprehensive security features to protect against malicious code injection, XSS attacks, SQL injection, and other vulnerabilities. This document outlines the security measures implemented and best practices for users.
|
||||
|
||||
## Security Features
|
||||
|
||||
### 1. Code Scanning
|
||||
|
||||
All user-generated code (JavaScript, Lua, JSON, HTML) is automatically scanned for security vulnerabilities before execution or saving.
|
||||
|
||||
#### Scan Levels
|
||||
|
||||
- **Safe**: No security issues detected
|
||||
- **Low**: Minor warnings that don't pose significant risk
|
||||
- **Medium**: Potential security concerns that should be reviewed
|
||||
- **High**: Serious security issues that require immediate attention
|
||||
- **Critical**: Severe vulnerabilities that block execution/saving
|
||||
|
||||
### 2. Sandboxed Lua Execution
|
||||
|
||||
Lua scripts are executed in a sandboxed environment with the following restrictions:
|
||||
|
||||
#### Disabled Functions & Modules
|
||||
|
||||
- **os module**: All operating system functions disabled
|
||||
- `os.execute`, `os.exit`, `os.remove`, `os.rename`, `os.tmpname`
|
||||
|
||||
- **io module**: All file I/O operations disabled
|
||||
- `io.popen`, `io.tmpfile`, `io.open`, `io.input`, `io.output`, `io.lines`
|
||||
|
||||
- **File loading**: Dynamic file loading disabled
|
||||
- `loadfile`, `dofile`
|
||||
|
||||
- **Package loading**: Dynamic library loading disabled
|
||||
- `package.loadlib`, `package.searchpath`, C library path cleared
|
||||
|
||||
#### Limited Functions
|
||||
|
||||
- **debug module**: Advanced debugging features limited
|
||||
- `debug.getfenv`, `debug.setfenv` disabled
|
||||
|
||||
- **Environment manipulation**: Global environment modifications restricted
|
||||
- Direct `_G` manipulation monitored
|
||||
|
||||
#### Safe Functions Available
|
||||
|
||||
The sandbox provides access to these safe functions:
|
||||
- Basic: `assert`, `error`, `ipairs`, `next`, `pairs`, `pcall`, `select`, `tonumber`, `tostring`, `type`, `unpack`, `xpcall`
|
||||
- Libraries: `string`, `table`, `math`, `bit32`
|
||||
- Logging: `print`, `log`
|
||||
- Context: `context.data`, `context.user`, `context.kv`
|
||||
|
||||
### 3. Execution Timeout
|
||||
|
||||
All Lua scripts have a maximum execution time of 5 seconds (configurable) to prevent infinite loops and resource exhaustion.
|
||||
|
||||
### 4. Pattern Detection
|
||||
|
||||
The security scanner detects the following malicious patterns:
|
||||
|
||||
#### JavaScript Threats
|
||||
|
||||
- **Code Execution**
|
||||
- `eval()`
|
||||
- Dynamic `Function()` constructor
|
||||
- `setTimeout/setInterval` with string arguments
|
||||
|
||||
- **XSS Vulnerabilities**
|
||||
- `innerHTML` assignments
|
||||
- `dangerouslySetInnerHTML`
|
||||
- `<script>` tag injection
|
||||
- `javascript:` protocol in URLs
|
||||
- Inline event handlers (`onclick`, `onerror`, etc.)
|
||||
|
||||
- **Prototype Pollution**
|
||||
- `__proto__` manipulation
|
||||
- `constructor.prototype` access
|
||||
|
||||
- **Remote Code Loading**
|
||||
- HTTP/HTTPS imports
|
||||
- Data URIs with executable content
|
||||
|
||||
#### Lua Threats
|
||||
|
||||
- **System Access**
|
||||
- OS module function calls
|
||||
- File I/O operations
|
||||
- Dynamic file loading
|
||||
- Package/library loading
|
||||
|
||||
- **Environment Manipulation**
|
||||
- Global environment modification
|
||||
- Metatable manipulation
|
||||
- Debug module advanced features
|
||||
|
||||
- **Infinite Loops**
|
||||
- `while true do` patterns without breaks
|
||||
- Recursive functions without termination
|
||||
|
||||
#### JSON Threats
|
||||
|
||||
- **Prototype Pollution**
|
||||
- `__proto__` in JSON keys
|
||||
- `constructor.prototype` manipulation
|
||||
|
||||
- **Script Injection**
|
||||
- `<script>` tags in JSON values
|
||||
- Executable content in data
|
||||
|
||||
#### SQL Injection (in strings)
|
||||
|
||||
- `DROP`, `DELETE`, `UPDATE`, `INSERT` commands
|
||||
- `UNION SELECT` attacks
|
||||
- Authentication bypass patterns (`OR '1'='1'`)
|
||||
- SQL comment patterns (`--`)
|
||||
|
||||
## User Interface
|
||||
|
||||
### Security Dialogs
|
||||
|
||||
When security issues are detected, users see a detailed dialog showing:
|
||||
|
||||
1. **Severity Level**: Visual indicator of threat level
|
||||
2. **Issue Count**: Number of security problems found
|
||||
3. **Detailed Issues**: Each issue includes:
|
||||
- Type (malicious, dangerous, suspicious, warning)
|
||||
- Severity level
|
||||
- Description of the problem
|
||||
- Line number (if applicable)
|
||||
- Code pattern that triggered the alert
|
||||
- Recommendation for fixing
|
||||
|
||||
### Security Scan Button
|
||||
|
||||
All code editors include a "Security Scan" button that allows manual scanning without attempting to save or execute code.
|
||||
|
||||
### Save/Execute Behavior
|
||||
|
||||
- **Critical Issues**: Block execution/saving completely
|
||||
- **High Issues**: Show warning dialog, require user confirmation to proceed
|
||||
- **Medium Issues**: Show warning dialog, allow proceeding with caution
|
||||
- **Low Issues**: Display toast notification but allow operation
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Administrators (God/SuperGod Users)
|
||||
|
||||
1. **Review All Code**: Always scan custom code before deploying
|
||||
2. **Test in Isolation**: Use the test/preview features before going live
|
||||
3. **Limit Permissions**: Grant minimal necessary permissions to users
|
||||
4. **Regular Audits**: Periodically review all stored scripts and code
|
||||
5. **Monitor Logs**: Check execution logs for suspicious patterns
|
||||
|
||||
### For Users
|
||||
|
||||
1. **Avoid Dangerous Patterns**: Don't use `eval()`, `innerHTML`, or other flagged patterns
|
||||
2. **Use Safe Alternatives**:
|
||||
- React JSX instead of string HTML
|
||||
- `textContent` instead of `innerHTML`
|
||||
- Function references instead of string code
|
||||
3. **Validate Input**: Always validate and sanitize user input
|
||||
4. **Use Context APIs**: Use provided `context.data`, `context.user` instead of global access
|
||||
|
||||
### For Lua Scripts
|
||||
|
||||
1. **Use Provided APIs**: Use `context.kv` for storage, not external methods
|
||||
2. **Limit Complexity**: Keep scripts simple and focused
|
||||
3. **Add Termination**: Ensure all loops have proper exit conditions
|
||||
4. **Log Appropriately**: Use `log()` for debugging but avoid logging sensitive data
|
||||
5. **Handle Errors**: Use `pcall()` for error handling
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Security Scanner (`security-scanner.ts`)
|
||||
|
||||
The `SecurityScanner` class provides methods to scan different code types:
|
||||
|
||||
```typescript
|
||||
const scanner = new SecurityScanner()
|
||||
|
||||
// Scan JavaScript
|
||||
const jsResult = scanner.scanJavaScript(code)
|
||||
|
||||
// Scan Lua
|
||||
const luaResult = scanner.scanLua(code)
|
||||
|
||||
// Scan JSON
|
||||
const jsonResult = scanner.scanJSON(jsonString)
|
||||
|
||||
// Scan HTML
|
||||
const htmlResult = scanner.scanHTML(htmlString)
|
||||
```
|
||||
|
||||
### Sandboxed Lua Engine (`sandboxed-lua-engine.ts`)
|
||||
|
||||
The `SandboxedLuaEngine` wraps the standard Lua engine with security measures:
|
||||
|
||||
```typescript
|
||||
const engine = createSandboxedLuaEngine(5000) // 5 second timeout
|
||||
|
||||
const result = await engine.executeWithSandbox(code, context)
|
||||
|
||||
// Result includes both execution result and security scan
|
||||
console.log(result.execution.success)
|
||||
console.log(result.security.severity)
|
||||
```
|
||||
|
||||
### Workflow Engine Integration
|
||||
|
||||
The workflow engine automatically uses the sandboxed Lua execution and reports security warnings in workflow execution results.
|
||||
|
||||
## Security Limitations
|
||||
|
||||
### What We Protect Against
|
||||
|
||||
✅ Common XSS attacks
|
||||
✅ SQL injection patterns
|
||||
✅ Prototype pollution
|
||||
✅ Remote code loading
|
||||
✅ File system access attempts
|
||||
✅ Infinite loops (with timeout)
|
||||
✅ OS command execution
|
||||
|
||||
### What We Don't Protect Against
|
||||
|
||||
❌ Logic bombs (code that appears safe but has malicious intent)
|
||||
❌ Social engineering attacks
|
||||
❌ Credential theft through legitimate-looking forms
|
||||
❌ Resource exhaustion within timeout limits
|
||||
❌ Obfuscated code (heavily encoded/encrypted patterns)
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
If you discover a security vulnerability in MetaBuilder:
|
||||
|
||||
1. **Do NOT** disclose it publicly
|
||||
2. Document the vulnerability with:
|
||||
- Steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
- Potential impact
|
||||
- Suggested fix (if applicable)
|
||||
3. Report to the system administrator or security team
|
||||
4. Allow reasonable time for patching before disclosure
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned security improvements:
|
||||
|
||||
- [ ] Rate limiting on code execution
|
||||
- [ ] Code complexity analysis
|
||||
- [ ] Machine learning-based anomaly detection
|
||||
- [ ] Encrypted storage for sensitive scripts
|
||||
- [ ] Audit logging for all code changes
|
||||
- [ ] Whitelist/blacklist for specific patterns
|
||||
- [ ] Content Security Policy (CSP) headers
|
||||
- [ ] Subresource Integrity (SRI) for external resources
|
||||
|
||||
## Version History
|
||||
|
||||
- **v1.0** (Current): Initial security implementation with code scanning and sandboxed Lua execution
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2024
|
||||
**Security Contact**: System Administrator
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useState } from 'react'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import Editor from '@monaco-editor/react'
|
||||
import { FloppyDisk, X } from '@phosphor-icons/react'
|
||||
import { FloppyDisk, X, ShieldCheck, Warning } from '@phosphor-icons/react'
|
||||
import { securityScanner, type SecurityScanResult } from '@/lib/security-scanner'
|
||||
import { SecurityWarningDialog } from '@/components/SecurityWarningDialog'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface CodeEditorProps {
|
||||
open: boolean
|
||||
@@ -14,56 +18,131 @@ interface CodeEditorProps {
|
||||
|
||||
export function CodeEditor({ open, onClose, code, onSave, componentName }: CodeEditorProps) {
|
||||
const [editorCode, setEditorCode] = useState(code || '// Write your custom code here\n')
|
||||
const [securityScanResult, setSecurityScanResult] = useState<SecurityScanResult | null>(null)
|
||||
const [showSecurityDialog, setShowSecurityDialog] = useState(false)
|
||||
const [pendingSave, setPendingSave] = useState(false)
|
||||
|
||||
const handleSave = () => {
|
||||
const scanResult = securityScanner.scanJavaScript(editorCode)
|
||||
setSecurityScanResult(scanResult)
|
||||
|
||||
if (scanResult.severity === 'critical') {
|
||||
setShowSecurityDialog(true)
|
||||
toast.error('Critical security issues detected - save blocked')
|
||||
return
|
||||
}
|
||||
|
||||
if (scanResult.severity === 'high' || scanResult.severity === 'medium') {
|
||||
setPendingSave(true)
|
||||
setShowSecurityDialog(true)
|
||||
toast.warning('Security issues detected - review before saving')
|
||||
return
|
||||
}
|
||||
|
||||
if (scanResult.issues.length > 0) {
|
||||
toast.warning(`${scanResult.issues.length} minor security warning(s)`)
|
||||
}
|
||||
|
||||
onSave(editorCode)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleForceSave = () => {
|
||||
setPendingSave(false)
|
||||
setShowSecurityDialog(false)
|
||||
onSave(editorCode)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleScan = () => {
|
||||
const scanResult = securityScanner.scanJavaScript(editorCode)
|
||||
setSecurityScanResult(scanResult)
|
||||
setShowSecurityDialog(true)
|
||||
|
||||
if (scanResult.safe) {
|
||||
toast.success('No security issues detected')
|
||||
} else {
|
||||
toast.warning(`${scanResult.issues.length} security issue(s) detected`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditorChange = (value: string | undefined) => {
|
||||
setEditorCode(value || '')
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-4xl h-[80vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Code Editor - {componentName}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Write custom JavaScript code for this component. Access component props via <code className="bg-muted px-1 rounded">props</code>.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-4xl h-[80vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Code Editor - {componentName}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Write custom JavaScript code for this component. Access component props via <code className="bg-muted px-1 rounded">props</code>.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 border rounded-md overflow-hidden">
|
||||
<Editor
|
||||
height="100%"
|
||||
defaultLanguage="javascript"
|
||||
value={editorCode}
|
||||
onChange={handleEditorChange}
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
lineNumbers: 'on',
|
||||
roundedSelection: false,
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
tabSize: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{securityScanResult && securityScanResult.severity !== 'safe' && securityScanResult.severity !== 'low' && !showSecurityDialog && (
|
||||
<Alert className="border-yellow-200 bg-yellow-50">
|
||||
<Warning className="h-5 w-5 text-yellow-600" weight="fill" />
|
||||
<AlertDescription className="text-yellow-800">
|
||||
{securityScanResult.issues.length} security {securityScanResult.issues.length === 1 ? 'issue' : 'issues'} detected.
|
||||
Click Security Scan to review.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<X className="mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="bg-primary">
|
||||
<FloppyDisk className="mr-2" />
|
||||
Save Code
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="flex-1 border rounded-md overflow-hidden">
|
||||
<Editor
|
||||
height="100%"
|
||||
defaultLanguage="javascript"
|
||||
value={editorCode}
|
||||
onChange={handleEditorChange}
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
lineNumbers: 'on',
|
||||
roundedSelection: false,
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
tabSize: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-2 pt-4">
|
||||
<Button variant="outline" onClick={handleScan}>
|
||||
<ShieldCheck className="mr-2" />
|
||||
Security Scan
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<X className="mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="bg-primary">
|
||||
<FloppyDisk className="mr-2" />
|
||||
Save Code
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{securityScanResult && (
|
||||
<SecurityWarningDialog
|
||||
open={showSecurityDialog}
|
||||
onOpenChange={setShowSecurityDialog}
|
||||
scanResult={securityScanResult}
|
||||
onProceed={pendingSave ? handleForceSave : undefined}
|
||||
onCancel={() => {
|
||||
setShowSecurityDialog(false)
|
||||
setPendingSave(false)
|
||||
}}
|
||||
codeType="JavaScript code"
|
||||
showProceedButton={pendingSave}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@ import { useState, useEffect } from 'react'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { FloppyDisk, X, Warning } from '@phosphor-icons/react'
|
||||
import { FloppyDisk, X, Warning, ShieldCheck } from '@phosphor-icons/react'
|
||||
import Editor from '@monaco-editor/react'
|
||||
import { securityScanner, type SecurityScanResult } from '@/lib/security-scanner'
|
||||
import { SecurityWarningDialog } from '@/components/SecurityWarningDialog'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface JsonEditorProps {
|
||||
open: boolean
|
||||
@@ -17,17 +20,42 @@ interface JsonEditorProps {
|
||||
export function JsonEditor({ open, onClose, title, value, onSave, schema }: JsonEditorProps) {
|
||||
const [jsonText, setJsonText] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [securityScanResult, setSecurityScanResult] = useState<SecurityScanResult | null>(null)
|
||||
const [showSecurityDialog, setShowSecurityDialog] = useState(false)
|
||||
const [pendingSave, setPendingSave] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setJsonText(JSON.stringify(value, null, 2))
|
||||
setError(null)
|
||||
setSecurityScanResult(null)
|
||||
}
|
||||
}, [open, value])
|
||||
|
||||
const handleSave = () => {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonText)
|
||||
|
||||
const scanResult = securityScanner.scanJSON(jsonText)
|
||||
setSecurityScanResult(scanResult)
|
||||
|
||||
if (scanResult.severity === 'critical') {
|
||||
setShowSecurityDialog(true)
|
||||
toast.error('Critical security issues detected - save blocked')
|
||||
return
|
||||
}
|
||||
|
||||
if (scanResult.severity === 'high' || scanResult.severity === 'medium') {
|
||||
setPendingSave(true)
|
||||
setShowSecurityDialog(true)
|
||||
toast.warning('Security issues detected - review before saving')
|
||||
return
|
||||
}
|
||||
|
||||
if (scanResult.issues.length > 0) {
|
||||
toast.warning(`${scanResult.issues.length} minor security warning(s)`)
|
||||
}
|
||||
|
||||
onSave(parsed)
|
||||
setError(null)
|
||||
onClose()
|
||||
@@ -36,6 +64,31 @@ export function JsonEditor({ open, onClose, title, value, onSave, schema }: Json
|
||||
}
|
||||
}
|
||||
|
||||
const handleForceSave = () => {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonText)
|
||||
onSave(parsed)
|
||||
setError(null)
|
||||
setPendingSave(false)
|
||||
setShowSecurityDialog(false)
|
||||
onClose()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Invalid JSON')
|
||||
}
|
||||
}
|
||||
|
||||
const handleScan = () => {
|
||||
const scanResult = securityScanner.scanJSON(jsonText)
|
||||
setSecurityScanResult(scanResult)
|
||||
setShowSecurityDialog(true)
|
||||
|
||||
if (scanResult.safe) {
|
||||
toast.success('No security issues detected')
|
||||
} else {
|
||||
toast.warning(`${scanResult.issues.length} security issue(s) detected`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFormat = () => {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonText)
|
||||
@@ -47,66 +100,97 @@ export function JsonEditor({ open, onClose, title, value, onSave, schema }: Json
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-5xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<Warning className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-5xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Editor
|
||||
height="600px"
|
||||
language="json"
|
||||
value={jsonText}
|
||||
onChange={(value) => {
|
||||
setJsonText(value || '')
|
||||
setError(null)
|
||||
}}
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
minimap: { enabled: true },
|
||||
fontSize: 14,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
lineNumbers: 'on',
|
||||
roundedSelection: true,
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
tabSize: 2,
|
||||
wordWrap: 'on',
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
bracketPairColorization: {
|
||||
enabled: true,
|
||||
},
|
||||
folding: true,
|
||||
foldingStrategy: 'indentation',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<Warning className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" onClick={handleFormat}>
|
||||
Format JSON
|
||||
</Button>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<X className="mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="bg-accent text-accent-foreground hover:bg-accent/90">
|
||||
<FloppyDisk className="mr-2" />
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{securityScanResult && securityScanResult.severity !== 'safe' && securityScanResult.severity !== 'low' && !showSecurityDialog && (
|
||||
<Alert className="border-yellow-200 bg-yellow-50">
|
||||
<Warning className="h-5 w-5 text-yellow-600" weight="fill" />
|
||||
<AlertDescription className="text-yellow-800">
|
||||
{securityScanResult.issues.length} security {securityScanResult.issues.length === 1 ? 'issue' : 'issues'} detected.
|
||||
Click Security Scan to review.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Editor
|
||||
height="600px"
|
||||
language="json"
|
||||
value={jsonText}
|
||||
onChange={(value) => {
|
||||
setJsonText(value || '')
|
||||
setError(null)
|
||||
}}
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
minimap: { enabled: true },
|
||||
fontSize: 14,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
lineNumbers: 'on',
|
||||
roundedSelection: true,
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
tabSize: 2,
|
||||
wordWrap: 'on',
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
bracketPairColorization: {
|
||||
enabled: true,
|
||||
},
|
||||
folding: true,
|
||||
foldingStrategy: 'indentation',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" onClick={handleScan}>
|
||||
<ShieldCheck className="mr-2" />
|
||||
Security Scan
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleFormat}>
|
||||
Format JSON
|
||||
</Button>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<X className="mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="bg-accent text-accent-foreground hover:bg-accent/90">
|
||||
<FloppyDisk className="mr-2" />
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{securityScanResult && (
|
||||
<SecurityWarningDialog
|
||||
open={showSecurityDialog}
|
||||
onOpenChange={setShowSecurityDialog}
|
||||
scanResult={securityScanResult}
|
||||
onProceed={pendingSave ? handleForceSave : undefined}
|
||||
onCancel={() => {
|
||||
setShowSecurityDialog(false)
|
||||
setPendingSave(false)
|
||||
}}
|
||||
codeType="JSON data"
|
||||
showProceedButton={pendingSave}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Plus, Trash, Play, CheckCircle, XCircle, FileCode, ArrowsOut, BookOpen } from '@phosphor-icons/react'
|
||||
import { Plus, Trash, Play, CheckCircle, XCircle, FileCode, ArrowsOut, BookOpen, ShieldCheck } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import { createLuaEngine, type LuaExecutionResult } from '@/lib/lua-engine'
|
||||
import { getLuaExampleCode, getLuaExamplesList } from '@/lib/lua-examples'
|
||||
@@ -20,6 +20,8 @@ import Editor, { useMonaco } from '@monaco-editor/react'
|
||||
import type { editor } from 'monaco-editor'
|
||||
import { LuaSnippetLibrary } from '@/components/LuaSnippetLibrary'
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'
|
||||
import { securityScanner, type SecurityScanResult } from '@/lib/security-scanner'
|
||||
import { SecurityWarningDialog } from '@/components/SecurityWarningDialog'
|
||||
|
||||
interface LuaEditorProps {
|
||||
scripts: LuaScript[]
|
||||
@@ -35,6 +37,8 @@ export function LuaEditor({ scripts, onScriptsChange }: LuaEditorProps) {
|
||||
const [isExecuting, setIsExecuting] = useState(false)
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
const [showSnippetLibrary, setShowSnippetLibrary] = useState(false)
|
||||
const [securityScanResult, setSecurityScanResult] = useState<SecurityScanResult | null>(null)
|
||||
const [showSecurityDialog, setShowSecurityDialog] = useState(false)
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||
const monaco = useMonaco()
|
||||
|
||||
@@ -174,6 +178,19 @@ export function LuaEditor({ scripts, onScriptsChange }: LuaEditorProps) {
|
||||
const handleTestScript = async () => {
|
||||
if (!currentScript) return
|
||||
|
||||
const scanResult = securityScanner.scanLua(currentScript.code)
|
||||
setSecurityScanResult(scanResult)
|
||||
|
||||
if (scanResult.severity === 'critical' || scanResult.severity === 'high') {
|
||||
setShowSecurityDialog(true)
|
||||
toast.warning('Security issues detected in script')
|
||||
return
|
||||
}
|
||||
|
||||
if (scanResult.severity === 'medium' && scanResult.issues.length > 0) {
|
||||
toast.warning(`${scanResult.issues.length} security warning(s) detected`)
|
||||
}
|
||||
|
||||
setIsExecuting(true)
|
||||
setTestOutput(null)
|
||||
|
||||
@@ -212,6 +229,64 @@ export function LuaEditor({ scripts, onScriptsChange }: LuaEditorProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleScanCode = () => {
|
||||
if (!currentScript) return
|
||||
|
||||
const scanResult = securityScanner.scanLua(currentScript.code)
|
||||
setSecurityScanResult(scanResult)
|
||||
setShowSecurityDialog(true)
|
||||
|
||||
if (scanResult.safe) {
|
||||
toast.success('No security issues detected')
|
||||
} else {
|
||||
toast.warning(`${scanResult.issues.length} security issue(s) detected`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleProceedWithExecution = () => {
|
||||
setShowSecurityDialog(false)
|
||||
if (!currentScript) return
|
||||
|
||||
setIsExecuting(true)
|
||||
setTestOutput(null)
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const engine = createLuaEngine()
|
||||
|
||||
const contextData: any = {}
|
||||
currentScript.parameters.forEach((param) => {
|
||||
contextData[param.name] = testInputs[param.name]
|
||||
})
|
||||
|
||||
const result = await engine.execute(currentScript.code, {
|
||||
data: contextData,
|
||||
user: { username: 'test_user', role: 'god' },
|
||||
log: (...args: any[]) => console.log('[Lua]', ...args)
|
||||
})
|
||||
|
||||
setTestOutput(result)
|
||||
|
||||
if (result.success) {
|
||||
toast.success('Script executed successfully')
|
||||
} else {
|
||||
toast.error('Script execution failed')
|
||||
}
|
||||
|
||||
engine.destroy()
|
||||
} catch (error) {
|
||||
toast.error('Execution error: ' + (error instanceof Error ? error.message : String(error)))
|
||||
setTestOutput({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
logs: []
|
||||
})
|
||||
} finally {
|
||||
setIsExecuting(false)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const handleAddParameter = () => {
|
||||
if (!currentScript) return
|
||||
|
||||
@@ -332,10 +407,16 @@ export function LuaEditor({ scripts, onScriptsChange }: LuaEditorProps) {
|
||||
<CardTitle>Edit Script: {currentScript.name}</CardTitle>
|
||||
<CardDescription>Write custom Lua logic</CardDescription>
|
||||
</div>
|
||||
<Button onClick={handleTestScript} disabled={isExecuting}>
|
||||
<Play className="mr-2" size={16} />
|
||||
{isExecuting ? 'Executing...' : 'Test Script'}
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleScanCode}>
|
||||
<ShieldCheck className="mr-2" size={16} />
|
||||
Security Scan
|
||||
</Button>
|
||||
<Button onClick={handleTestScript} disabled={isExecuting}>
|
||||
<Play className="mr-2" size={16} />
|
||||
{isExecuting ? 'Executing...' : 'Test Script'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
@@ -588,6 +669,18 @@ export function LuaEditor({ scripts, onScriptsChange }: LuaEditorProps) {
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{securityScanResult && (
|
||||
<SecurityWarningDialog
|
||||
open={showSecurityDialog}
|
||||
onOpenChange={setShowSecurityDialog}
|
||||
scanResult={securityScanResult}
|
||||
onProceed={handleProceedWithExecution}
|
||||
onCancel={() => setShowSecurityDialog(false)}
|
||||
codeType="Lua script"
|
||||
showProceedButton={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
235
src/components/SecurityWarningDialog.tsx
Normal file
235
src/components/SecurityWarningDialog.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { ShieldWarning, Warning, Info, CheckCircle } from '@phosphor-icons/react'
|
||||
import type { SecurityScanResult, SecurityIssue } from '@/lib/security-scanner'
|
||||
import { getSeverityColor, getSeverityIcon } from '@/lib/security-scanner'
|
||||
|
||||
interface SecurityWarningDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
scanResult: SecurityScanResult
|
||||
onProceed?: () => void
|
||||
onCancel?: () => void
|
||||
codeType?: string
|
||||
showProceedButton?: boolean
|
||||
}
|
||||
|
||||
export function SecurityWarningDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
scanResult,
|
||||
onProceed,
|
||||
onCancel,
|
||||
codeType = 'code',
|
||||
showProceedButton = false
|
||||
}: SecurityWarningDialogProps) {
|
||||
const [acknowledged, setAcknowledged] = useState(false)
|
||||
|
||||
const handleProceed = () => {
|
||||
if (onProceed) {
|
||||
onProceed()
|
||||
}
|
||||
setAcknowledged(false)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
if (onCancel) {
|
||||
onCancel()
|
||||
}
|
||||
setAcknowledged(false)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
const getSeverityBadgeVariant = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
return 'destructive'
|
||||
case 'high':
|
||||
return 'destructive'
|
||||
case 'medium':
|
||||
return 'default'
|
||||
case 'low':
|
||||
return 'secondary'
|
||||
default:
|
||||
return 'outline'
|
||||
}
|
||||
}
|
||||
|
||||
const getIcon = () => {
|
||||
switch (scanResult.severity) {
|
||||
case 'critical':
|
||||
case 'high':
|
||||
return <ShieldWarning className="w-12 h-12 text-red-600" weight="fill" />
|
||||
case 'medium':
|
||||
return <Warning className="w-12 h-12 text-yellow-600" weight="fill" />
|
||||
case 'low':
|
||||
return <Info className="w-12 h-12 text-blue-600" weight="fill" />
|
||||
default:
|
||||
return <CheckCircle className="w-12 h-12 text-green-600" weight="fill" />
|
||||
}
|
||||
}
|
||||
|
||||
const getTitle = () => {
|
||||
if (scanResult.safe) {
|
||||
return 'Code Security Check Passed'
|
||||
}
|
||||
switch (scanResult.severity) {
|
||||
case 'critical':
|
||||
return 'CRITICAL SECURITY THREAT DETECTED'
|
||||
case 'high':
|
||||
return 'High-Risk Security Issues Detected'
|
||||
case 'medium':
|
||||
return 'Security Warnings Detected'
|
||||
case 'low':
|
||||
return 'Minor Security Concerns'
|
||||
default:
|
||||
return 'Security Scan Complete'
|
||||
}
|
||||
}
|
||||
|
||||
const getDescription = () => {
|
||||
if (scanResult.safe) {
|
||||
return `Your ${codeType} has been scanned and appears to be safe.`
|
||||
}
|
||||
return `Your ${codeType} contains ${scanResult.issues.length} security ${scanResult.issues.length === 1 ? 'issue' : 'issues'} that require attention.`
|
||||
}
|
||||
|
||||
const groupedIssues = scanResult.issues.reduce((acc, issue) => {
|
||||
if (!acc[issue.severity]) {
|
||||
acc[issue.severity] = []
|
||||
}
|
||||
acc[issue.severity].push(issue)
|
||||
return acc
|
||||
}, {} as Record<string, SecurityIssue[]>)
|
||||
|
||||
const severityOrder = ['critical', 'high', 'medium', 'low']
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
{getIcon()}
|
||||
<div className="flex-1">
|
||||
<DialogTitle className="text-2xl">{getTitle()}</DialogTitle>
|
||||
<DialogDescription className="text-base mt-1">
|
||||
{getDescription()}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="max-h-[50vh] pr-4">
|
||||
{scanResult.safe ? (
|
||||
<Alert className="border-green-200 bg-green-50">
|
||||
<CheckCircle className="h-5 w-5 text-green-600" weight="fill" />
|
||||
<AlertDescription className="text-green-800">
|
||||
No security issues detected. Your code follows security best practices.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{severityOrder.map(severity => {
|
||||
const issues = groupedIssues[severity]
|
||||
if (!issues || issues.length === 0) return null
|
||||
|
||||
return (
|
||||
<div key={severity} className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={getSeverityBadgeVariant(severity)} className="uppercase">
|
||||
{getSeverityIcon(severity)} {severity}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{issues.length} {issues.length === 1 ? 'issue' : 'issues'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{issues.map((issue, idx) => (
|
||||
<Alert key={idx} className={getSeverityColor(issue.severity)}>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold">{issue.message}</p>
|
||||
{issue.line && (
|
||||
<p className="text-sm opacity-75 mt-1">
|
||||
Line {issue.line}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
{issue.pattern}
|
||||
</Badge>
|
||||
</div>
|
||||
{issue.recommendation && (
|
||||
<>
|
||||
<Separator className="my-2" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium mb-1">Recommendation:</p>
|
||||
<p className="opacity-90">{issue.recommendation}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Alert>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{(scanResult.severity === 'critical' || scanResult.severity === 'high') && (
|
||||
<Alert className="border-red-300 bg-red-50 mt-4">
|
||||
<ShieldWarning className="h-5 w-5 text-red-600" weight="fill" />
|
||||
<AlertDescription className="text-red-800">
|
||||
<p className="font-semibold mb-2">Security Alert</p>
|
||||
<p>
|
||||
{scanResult.severity === 'critical'
|
||||
? 'This code contains CRITICAL security vulnerabilities that could compromise system security, steal data, or execute malicious actions. It is strongly recommended NOT to proceed.'
|
||||
: 'This code contains HIGH-RISK security issues that could lead to vulnerabilities or unexpected behavior. Carefully review and fix these issues before proceeding.'
|
||||
}
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter className="flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{scanResult.safe ? 'Close' : 'Cancel'}
|
||||
</Button>
|
||||
|
||||
{!scanResult.safe && showProceedButton && (
|
||||
<Button
|
||||
variant={scanResult.severity === 'critical' ? 'destructive' : 'default'}
|
||||
onClick={handleProceed}
|
||||
disabled={scanResult.severity === 'critical' && !acknowledged}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{scanResult.severity === 'critical' ? 'Force Proceed (Not Recommended)' : 'Proceed Anyway'}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
181
src/lib/sandboxed-lua-engine.ts
Normal file
181
src/lib/sandboxed-lua-engine.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { LuaEngine, LuaExecutionContext, LuaExecutionResult } from './lua-engine'
|
||||
import { securityScanner, SecurityScanResult } from './security-scanner'
|
||||
import * as fengari from 'fengari-web'
|
||||
|
||||
const lua = fengari.lua
|
||||
const lauxlib = fengari.lauxlib
|
||||
const lualib = fengari.lualib
|
||||
|
||||
export interface SandboxedLuaResult {
|
||||
execution: LuaExecutionResult
|
||||
security: SecurityScanResult
|
||||
}
|
||||
|
||||
export class SandboxedLuaEngine {
|
||||
private engine: LuaEngine | null = null
|
||||
private executionTimeout: number = 5000
|
||||
private maxMemory: number = 10 * 1024 * 1024
|
||||
|
||||
constructor(timeout: number = 5000) {
|
||||
this.executionTimeout = timeout
|
||||
}
|
||||
|
||||
async executeWithSandbox(code: string, context: LuaExecutionContext = {}): Promise<SandboxedLuaResult> {
|
||||
const securityResult = securityScanner.scanLua(code)
|
||||
|
||||
if (securityResult.severity === 'critical') {
|
||||
return {
|
||||
execution: {
|
||||
success: false,
|
||||
error: 'Code blocked due to critical security issues. Please review the security warnings.',
|
||||
logs: []
|
||||
},
|
||||
security: securityResult
|
||||
}
|
||||
}
|
||||
|
||||
this.engine = new LuaEngine()
|
||||
|
||||
this.disableDangerousFunctions()
|
||||
this.setupSandboxedEnvironment()
|
||||
|
||||
const executionPromise = this.executeWithTimeout(code, context)
|
||||
|
||||
try {
|
||||
const result = await executionPromise
|
||||
|
||||
return {
|
||||
execution: result,
|
||||
security: securityResult
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
execution: {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Execution failed',
|
||||
logs: []
|
||||
},
|
||||
security: securityResult
|
||||
}
|
||||
} finally {
|
||||
if (this.engine) {
|
||||
this.engine.destroy()
|
||||
this.engine = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private disableDangerousFunctions() {
|
||||
if (!this.engine) return
|
||||
|
||||
const L = (this.engine as any).L
|
||||
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setglobal(L, fengari.to_luastring('os'))
|
||||
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setglobal(L, fengari.to_luastring('io'))
|
||||
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setglobal(L, fengari.to_luastring('loadfile'))
|
||||
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setglobal(L, fengari.to_luastring('dofile'))
|
||||
|
||||
lua.lua_getglobal(L, fengari.to_luastring('package'))
|
||||
if (!lua.lua_isnil(L, -1)) {
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('loadlib'))
|
||||
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('searchpath'))
|
||||
|
||||
lua.lua_pushstring(L, fengari.to_luastring(''))
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('cpath'))
|
||||
}
|
||||
lua.lua_pop(L, 1)
|
||||
|
||||
lua.lua_getglobal(L, fengari.to_luastring('debug'))
|
||||
if (!lua.lua_isnil(L, -1)) {
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('getfenv'))
|
||||
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('setfenv'))
|
||||
}
|
||||
lua.lua_pop(L, 1)
|
||||
}
|
||||
|
||||
private setupSandboxedEnvironment() {
|
||||
if (!this.engine) return
|
||||
|
||||
const L = (this.engine as any).L
|
||||
|
||||
lua.lua_newtable(L)
|
||||
|
||||
const safeFunctions = [
|
||||
'assert', 'error', 'ipairs', 'next', 'pairs', 'pcall', 'select',
|
||||
'tonumber', 'tostring', 'type', 'unpack', 'xpcall',
|
||||
'string', 'table', 'math', 'bit32'
|
||||
]
|
||||
|
||||
for (const funcName of safeFunctions) {
|
||||
lua.lua_getglobal(L, fengari.to_luastring(funcName))
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring(funcName))
|
||||
}
|
||||
|
||||
lua.lua_getglobal(L, fengari.to_luastring('print'))
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('print'))
|
||||
|
||||
lua.lua_getglobal(L, fengari.to_luastring('log'))
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('log'))
|
||||
|
||||
lua.lua_pushvalue(L, -1)
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('_G'))
|
||||
|
||||
lua.lua_setglobal(L, fengari.to_luastring('_ENV'))
|
||||
}
|
||||
|
||||
private async executeWithTimeout(code: string, context: LuaExecutionContext): Promise<LuaExecutionResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (this.engine) {
|
||||
this.engine.destroy()
|
||||
this.engine = null
|
||||
}
|
||||
reject(new Error(`Execution timeout: exceeded ${this.executionTimeout}ms limit`))
|
||||
}, this.executionTimeout)
|
||||
|
||||
if (!this.engine) {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error('Engine not initialized'))
|
||||
return
|
||||
}
|
||||
|
||||
this.engine.execute(code, context)
|
||||
.then(result => {
|
||||
clearTimeout(timeout)
|
||||
resolve(result)
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(timeout)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
setExecutionTimeout(timeout: number) {
|
||||
this.executionTimeout = timeout
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.engine) {
|
||||
this.engine.destroy()
|
||||
this.engine = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createSandboxedLuaEngine(timeout: number = 5000): SandboxedLuaEngine {
|
||||
return new SandboxedLuaEngine(timeout)
|
||||
}
|
||||
565
src/lib/security-scanner.ts
Normal file
565
src/lib/security-scanner.ts
Normal file
@@ -0,0 +1,565 @@
|
||||
export interface SecurityScanResult {
|
||||
safe: boolean
|
||||
severity: 'safe' | 'low' | 'medium' | 'high' | 'critical'
|
||||
issues: SecurityIssue[]
|
||||
sanitizedCode?: string
|
||||
}
|
||||
|
||||
export interface SecurityIssue {
|
||||
type: 'malicious' | 'suspicious' | 'dangerous' | 'warning'
|
||||
severity: 'low' | 'medium' | 'high' | 'critical'
|
||||
message: string
|
||||
pattern: string
|
||||
line?: number
|
||||
recommendation?: string
|
||||
}
|
||||
|
||||
const MALICIOUS_PATTERNS = [
|
||||
{
|
||||
pattern: /eval\s*\(/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'critical' as const,
|
||||
message: 'Use of eval() detected - can execute arbitrary code',
|
||||
recommendation: 'Use safe alternatives like JSON.parse() or Function constructor with strict validation'
|
||||
},
|
||||
{
|
||||
pattern: /Function\s*\(/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'high' as const,
|
||||
message: 'Dynamic Function constructor detected',
|
||||
recommendation: 'Avoid dynamic code generation or use with extreme caution'
|
||||
},
|
||||
{
|
||||
pattern: /innerHTML\s*=/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'high' as const,
|
||||
message: 'innerHTML assignment detected - XSS vulnerability risk',
|
||||
recommendation: 'Use textContent, createElement, or React JSX instead'
|
||||
},
|
||||
{
|
||||
pattern: /dangerouslySetInnerHTML/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'high' as const,
|
||||
message: 'dangerouslySetInnerHTML detected - XSS vulnerability risk',
|
||||
recommendation: 'Sanitize HTML content or use safe alternatives'
|
||||
},
|
||||
{
|
||||
pattern: /document\.write\s*\(/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'medium' as const,
|
||||
message: 'document.write() detected - can cause security issues',
|
||||
recommendation: 'Use DOM manipulation methods instead'
|
||||
},
|
||||
{
|
||||
pattern: /\.call\s*\(\s*window/gi,
|
||||
type: 'suspicious' as const,
|
||||
severity: 'medium' as const,
|
||||
message: 'Calling functions with window context',
|
||||
recommendation: 'Be careful with context manipulation'
|
||||
},
|
||||
{
|
||||
pattern: /\.apply\s*\(\s*window/gi,
|
||||
type: 'suspicious' as const,
|
||||
severity: 'medium' as const,
|
||||
message: 'Applying functions with window context',
|
||||
recommendation: 'Be careful with context manipulation'
|
||||
},
|
||||
{
|
||||
pattern: /__proto__/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'critical' as const,
|
||||
message: 'Prototype pollution attempt detected',
|
||||
recommendation: 'Never manipulate __proto__ directly'
|
||||
},
|
||||
{
|
||||
pattern: /constructor\s*\[\s*['"]prototype['"]\s*\]/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'critical' as const,
|
||||
message: 'Prototype manipulation detected',
|
||||
recommendation: 'Use Object.create() or proper class syntax'
|
||||
},
|
||||
{
|
||||
pattern: /import\s+.*\s+from\s+['"]https?:/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'critical' as const,
|
||||
message: 'Remote code import detected',
|
||||
recommendation: 'Only import from trusted, local sources'
|
||||
},
|
||||
{
|
||||
pattern: /<script[^>]*>/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'critical' as const,
|
||||
message: 'Script tag injection detected',
|
||||
recommendation: 'Never inject script tags dynamically'
|
||||
},
|
||||
{
|
||||
pattern: /on(click|load|error|mouseover|mouseout|focus|blur)\s*=/gi,
|
||||
type: 'suspicious' as const,
|
||||
severity: 'medium' as const,
|
||||
message: 'Inline event handler detected',
|
||||
recommendation: 'Use addEventListener or React event handlers'
|
||||
},
|
||||
{
|
||||
pattern: /javascript:\s*/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'high' as const,
|
||||
message: 'javascript: protocol detected',
|
||||
recommendation: 'Never use javascript: protocol in URLs'
|
||||
},
|
||||
{
|
||||
pattern: /data:\s*text\/html/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'high' as const,
|
||||
message: 'Data URI with HTML detected',
|
||||
recommendation: 'Avoid data URIs with executable content'
|
||||
},
|
||||
{
|
||||
pattern: /setTimeout\s*\(\s*['"`]/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'high' as const,
|
||||
message: 'setTimeout with string argument detected',
|
||||
recommendation: 'Use setTimeout with function reference instead'
|
||||
},
|
||||
{
|
||||
pattern: /setInterval\s*\(\s*['"`]/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'high' as const,
|
||||
message: 'setInterval with string argument detected',
|
||||
recommendation: 'Use setInterval with function reference instead'
|
||||
},
|
||||
{
|
||||
pattern: /localStorage|sessionStorage/gi,
|
||||
type: 'warning' as const,
|
||||
severity: 'low' as const,
|
||||
message: 'Local/session storage usage detected',
|
||||
recommendation: 'Use useKV hook for persistent data instead'
|
||||
},
|
||||
{
|
||||
pattern: /crypto\.subtle|atob|btoa/gi,
|
||||
type: 'warning' as const,
|
||||
severity: 'low' as const,
|
||||
message: 'Cryptographic operation detected',
|
||||
recommendation: 'Ensure proper key management and secure practices'
|
||||
},
|
||||
{
|
||||
pattern: /XMLHttpRequest|fetch\s*\(\s*['"`]http/gi,
|
||||
type: 'warning' as const,
|
||||
severity: 'medium' as const,
|
||||
message: 'External HTTP request detected',
|
||||
recommendation: 'Ensure CORS and security headers are properly configured'
|
||||
},
|
||||
{
|
||||
pattern: /window\.open/gi,
|
||||
type: 'suspicious' as const,
|
||||
severity: 'medium' as const,
|
||||
message: 'window.open detected',
|
||||
recommendation: 'Be cautious with popup windows'
|
||||
},
|
||||
{
|
||||
pattern: /location\.href\s*=/gi,
|
||||
type: 'suspicious' as const,
|
||||
severity: 'medium' as const,
|
||||
message: 'Direct location manipulation detected',
|
||||
recommendation: 'Use React Router or validate URLs carefully'
|
||||
},
|
||||
{
|
||||
pattern: /require\s*\(\s*[^'"`]/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'high' as const,
|
||||
message: 'Dynamic require() detected',
|
||||
recommendation: 'Use static imports only'
|
||||
},
|
||||
{
|
||||
pattern: /\.exec\s*\(|child_process|spawn|fork|execFile/gi,
|
||||
type: 'malicious' as const,
|
||||
severity: 'critical' as const,
|
||||
message: 'System command execution attempt detected',
|
||||
recommendation: 'This is not allowed in browser environment'
|
||||
},
|
||||
{
|
||||
pattern: /fs\.|path\.|os\./gi,
|
||||
type: 'malicious' as const,
|
||||
severity: 'critical' as const,
|
||||
message: 'Node.js system module usage detected',
|
||||
recommendation: 'File system access not allowed in browser'
|
||||
},
|
||||
{
|
||||
pattern: /process\.env|process\.exit/gi,
|
||||
type: 'suspicious' as const,
|
||||
severity: 'medium' as const,
|
||||
message: 'Process manipulation detected',
|
||||
recommendation: 'Not applicable in browser environment'
|
||||
}
|
||||
]
|
||||
|
||||
const LUA_MALICIOUS_PATTERNS = [
|
||||
{
|
||||
pattern: /os\.(execute|exit|remove|rename|tmpname)/gi,
|
||||
type: 'malicious' as const,
|
||||
severity: 'critical' as const,
|
||||
message: 'Lua OS module system call detected',
|
||||
recommendation: 'OS module access is disabled for security'
|
||||
},
|
||||
{
|
||||
pattern: /io\.(popen|tmpfile|open|input|output|lines)/gi,
|
||||
type: 'malicious' as const,
|
||||
severity: 'critical' as const,
|
||||
message: 'Lua file I/O operation detected',
|
||||
recommendation: 'File system access is disabled for security'
|
||||
},
|
||||
{
|
||||
pattern: /loadfile|dofile/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'critical' as const,
|
||||
message: 'Lua file loading function detected',
|
||||
recommendation: 'File loading is disabled for security'
|
||||
},
|
||||
{
|
||||
pattern: /package\.(loadlib|searchpath|cpath)/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'critical' as const,
|
||||
message: 'Lua dynamic library loading detected',
|
||||
recommendation: 'Dynamic library loading is disabled'
|
||||
},
|
||||
{
|
||||
pattern: /debug\.(getinfo|setmetatable|getfenv|setfenv)/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'high' as const,
|
||||
message: 'Lua debug module advanced features detected',
|
||||
recommendation: 'Limited debug functionality available'
|
||||
},
|
||||
{
|
||||
pattern: /loadstring\s*\(/gi,
|
||||
type: 'dangerous' as const,
|
||||
severity: 'high' as const,
|
||||
message: 'Lua dynamic code execution detected',
|
||||
recommendation: 'Use with extreme caution'
|
||||
},
|
||||
{
|
||||
pattern: /\.\.\s*[\[\]]/gi,
|
||||
type: 'suspicious' as const,
|
||||
severity: 'medium' as const,
|
||||
message: 'Potential Lua table manipulation',
|
||||
recommendation: 'Ensure proper validation'
|
||||
},
|
||||
{
|
||||
pattern: /_G\s*\[/gi,
|
||||
type: 'suspicious' as const,
|
||||
severity: 'high' as const,
|
||||
message: 'Global environment manipulation detected',
|
||||
recommendation: 'Avoid modifying global environment'
|
||||
},
|
||||
{
|
||||
pattern: /getmetatable|setmetatable/gi,
|
||||
type: 'suspicious' as const,
|
||||
severity: 'medium' as const,
|
||||
message: 'Metatable manipulation detected',
|
||||
recommendation: 'Use carefully to avoid security issues'
|
||||
},
|
||||
{
|
||||
pattern: /while\s+true\s+do/gi,
|
||||
type: 'warning' as const,
|
||||
severity: 'medium' as const,
|
||||
message: 'Infinite loop detected',
|
||||
recommendation: 'Ensure proper break conditions exist'
|
||||
},
|
||||
{
|
||||
pattern: /function\s+\w+\s*\([^)]*\)\s*\1\s*\(/gi,
|
||||
type: 'warning' as const,
|
||||
severity: 'low' as const,
|
||||
message: 'Potential recursive function',
|
||||
recommendation: 'Ensure recursion has proper termination'
|
||||
}
|
||||
]
|
||||
|
||||
const SQL_INJECTION_PATTERNS = [
|
||||
{
|
||||
pattern: /;\s*(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE)\s+/gi,
|
||||
type: 'malicious' as const,
|
||||
severity: 'critical' as const,
|
||||
message: 'SQL injection attempt detected',
|
||||
recommendation: 'Use parameterized queries'
|
||||
},
|
||||
{
|
||||
pattern: /UNION\s+SELECT/gi,
|
||||
type: 'malicious' as const,
|
||||
severity: 'critical' as const,
|
||||
message: 'SQL UNION injection attempt',
|
||||
recommendation: 'Use parameterized queries'
|
||||
},
|
||||
{
|
||||
pattern: /'[\s]*OR[\s]*'1'[\s]*=[\s]*'1/gi,
|
||||
type: 'malicious' as const,
|
||||
severity: 'critical' as const,
|
||||
message: 'SQL authentication bypass attempt',
|
||||
recommendation: 'Never concatenate user input into SQL'
|
||||
},
|
||||
{
|
||||
pattern: /--[\s]*$/gm,
|
||||
type: 'suspicious' as const,
|
||||
severity: 'high' as const,
|
||||
message: 'SQL comment pattern detected',
|
||||
recommendation: 'May indicate SQL injection attempt'
|
||||
}
|
||||
]
|
||||
|
||||
export class SecurityScanner {
|
||||
scanJavaScript(code: string): SecurityScanResult {
|
||||
const issues: SecurityIssue[] = []
|
||||
const lines = code.split('\n')
|
||||
|
||||
for (const pattern of MALICIOUS_PATTERNS) {
|
||||
const matches = code.matchAll(new RegExp(pattern.pattern.source, pattern.pattern.flags))
|
||||
for (const match of matches) {
|
||||
const lineNumber = this.getLineNumber(code, match.index || 0)
|
||||
issues.push({
|
||||
type: pattern.type,
|
||||
severity: pattern.severity,
|
||||
message: pattern.message,
|
||||
pattern: match[0],
|
||||
line: lineNumber,
|
||||
recommendation: pattern.recommendation
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const pattern of SQL_INJECTION_PATTERNS) {
|
||||
const matches = code.matchAll(new RegExp(pattern.pattern.source, pattern.pattern.flags))
|
||||
for (const match of matches) {
|
||||
const lineNumber = this.getLineNumber(code, match.index || 0)
|
||||
issues.push({
|
||||
type: pattern.type,
|
||||
severity: pattern.severity,
|
||||
message: pattern.message,
|
||||
pattern: match[0],
|
||||
line: lineNumber,
|
||||
recommendation: pattern.recommendation
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const severity = this.calculateOverallSeverity(issues)
|
||||
const safe = severity === 'safe' || severity === 'low'
|
||||
|
||||
return {
|
||||
safe,
|
||||
severity,
|
||||
issues,
|
||||
sanitizedCode: safe ? code : undefined
|
||||
}
|
||||
}
|
||||
|
||||
scanLua(code: string): SecurityScanResult {
|
||||
const issues: SecurityIssue[] = []
|
||||
|
||||
for (const pattern of LUA_MALICIOUS_PATTERNS) {
|
||||
const matches = code.matchAll(new RegExp(pattern.pattern.source, pattern.pattern.flags))
|
||||
for (const match of matches) {
|
||||
const lineNumber = this.getLineNumber(code, match.index || 0)
|
||||
issues.push({
|
||||
type: pattern.type,
|
||||
severity: pattern.severity,
|
||||
message: pattern.message,
|
||||
pattern: match[0],
|
||||
line: lineNumber,
|
||||
recommendation: pattern.recommendation
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const severity = this.calculateOverallSeverity(issues)
|
||||
const safe = severity === 'safe' || severity === 'low'
|
||||
|
||||
return {
|
||||
safe,
|
||||
severity,
|
||||
issues,
|
||||
sanitizedCode: safe ? code : undefined
|
||||
}
|
||||
}
|
||||
|
||||
scanJSON(jsonString: string): SecurityScanResult {
|
||||
const issues: SecurityIssue[] = []
|
||||
|
||||
try {
|
||||
JSON.parse(jsonString)
|
||||
} catch (error) {
|
||||
issues.push({
|
||||
type: 'warning',
|
||||
severity: 'medium',
|
||||
message: 'Invalid JSON format',
|
||||
pattern: 'JSON parse error',
|
||||
recommendation: 'Ensure JSON is properly formatted'
|
||||
})
|
||||
}
|
||||
|
||||
const protoPollution = /__proto__|constructor\s*\[\s*['"]prototype['"]\s*\]/gi
|
||||
if (protoPollution.test(jsonString)) {
|
||||
issues.push({
|
||||
type: 'malicious',
|
||||
severity: 'critical',
|
||||
message: 'Prototype pollution attempt in JSON',
|
||||
pattern: '__proto__',
|
||||
recommendation: 'Remove prototype manipulation from JSON'
|
||||
})
|
||||
}
|
||||
|
||||
if (jsonString.includes('<script')) {
|
||||
issues.push({
|
||||
type: 'malicious',
|
||||
severity: 'critical',
|
||||
message: 'Script tag in JSON data',
|
||||
pattern: '<script>',
|
||||
recommendation: 'Remove all HTML/script content from JSON'
|
||||
})
|
||||
}
|
||||
|
||||
const severity = this.calculateOverallSeverity(issues)
|
||||
const safe = severity === 'safe' || severity === 'low'
|
||||
|
||||
return {
|
||||
safe,
|
||||
severity,
|
||||
issues,
|
||||
sanitizedCode: safe ? jsonString : undefined
|
||||
}
|
||||
}
|
||||
|
||||
scanHTML(html: string): SecurityScanResult {
|
||||
const issues: SecurityIssue[] = []
|
||||
|
||||
const scriptTagPattern = /<script[^>]*>.*?<\/script>/gis
|
||||
const matches = html.matchAll(scriptTagPattern)
|
||||
for (const match of matches) {
|
||||
issues.push({
|
||||
type: 'dangerous',
|
||||
severity: 'critical',
|
||||
message: 'Script tag detected in HTML',
|
||||
pattern: match[0].substring(0, 50) + '...',
|
||||
recommendation: 'Remove script tags or use proper React components'
|
||||
})
|
||||
}
|
||||
|
||||
const inlineEventPattern = /on(click|load|error|mouseover|mouseout|focus|blur|submit)\s*=/gi
|
||||
const inlineMatches = html.matchAll(inlineEventPattern)
|
||||
for (const match of inlineMatches) {
|
||||
issues.push({
|
||||
type: 'dangerous',
|
||||
severity: 'high',
|
||||
message: 'Inline event handler in HTML',
|
||||
pattern: match[0],
|
||||
recommendation: 'Use React event handlers instead'
|
||||
})
|
||||
}
|
||||
|
||||
const javascriptProtocol = /href\s*=\s*['"]javascript:/gi
|
||||
if (javascriptProtocol.test(html)) {
|
||||
issues.push({
|
||||
type: 'dangerous',
|
||||
severity: 'critical',
|
||||
message: 'javascript: protocol in href',
|
||||
pattern: 'javascript:',
|
||||
recommendation: 'Use proper URLs or event handlers'
|
||||
})
|
||||
}
|
||||
|
||||
const iframePattern = /<iframe[^>]*>/gi
|
||||
const iframeMatches = html.matchAll(iframePattern)
|
||||
for (const match of iframeMatches) {
|
||||
if (!match[0].includes('sandbox=')) {
|
||||
issues.push({
|
||||
type: 'suspicious',
|
||||
severity: 'medium',
|
||||
message: 'Iframe without sandbox attribute',
|
||||
pattern: match[0],
|
||||
recommendation: 'Add sandbox attribute to iframes for security'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const severity = this.calculateOverallSeverity(issues)
|
||||
const safe = severity === 'safe' || severity === 'low'
|
||||
|
||||
return {
|
||||
safe,
|
||||
severity,
|
||||
issues
|
||||
}
|
||||
}
|
||||
|
||||
private getLineNumber(code: string, index: number): number {
|
||||
return code.substring(0, index).split('\n').length
|
||||
}
|
||||
|
||||
private calculateOverallSeverity(issues: SecurityIssue[]): 'safe' | 'low' | 'medium' | 'high' | 'critical' {
|
||||
if (issues.length === 0) return 'safe'
|
||||
|
||||
const hasCritical = issues.some(i => i.severity === 'critical')
|
||||
const hasHigh = issues.some(i => i.severity === 'high')
|
||||
const hasMedium = issues.some(i => i.severity === 'medium')
|
||||
const hasLow = issues.some(i => i.severity === 'low')
|
||||
|
||||
if (hasCritical) return 'critical'
|
||||
if (hasHigh) return 'high'
|
||||
if (hasMedium) return 'medium'
|
||||
if (hasLow) return 'low'
|
||||
|
||||
return 'safe'
|
||||
}
|
||||
|
||||
sanitizeInput(input: string, type: 'text' | 'html' | 'json' | 'javascript' | 'lua' = 'text'): string {
|
||||
let sanitized = input
|
||||
|
||||
if (type === 'text') {
|
||||
sanitized = sanitized.replace(/<script[^>]*>.*?<\/script>/gis, '')
|
||||
sanitized = sanitized.replace(/on\w+\s*=/gi, '')
|
||||
sanitized = sanitized.replace(/javascript:/gi, '')
|
||||
}
|
||||
|
||||
if (type === 'html') {
|
||||
sanitized = sanitized.replace(/<script[^>]*>.*?<\/script>/gis, '')
|
||||
sanitized = sanitized.replace(/on\w+\s*=/gi, '')
|
||||
sanitized = sanitized.replace(/javascript:/gi, '')
|
||||
sanitized = sanitized.replace(/data:\s*text\/html/gi, '')
|
||||
}
|
||||
|
||||
if (type === 'json') {
|
||||
sanitized = sanitized.replace(/__proto__/gi, '_proto_')
|
||||
sanitized = sanitized.replace(/constructor\s*\[\s*['"]prototype['"]\s*\]/gi, '')
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
}
|
||||
|
||||
export const securityScanner = new SecurityScanner()
|
||||
|
||||
export function getSeverityColor(severity: string): string {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
return 'text-red-600 bg-red-50 border-red-200'
|
||||
case 'high':
|
||||
return 'text-orange-600 bg-orange-50 border-orange-200'
|
||||
case 'medium':
|
||||
return 'text-yellow-600 bg-yellow-50 border-yellow-200'
|
||||
case 'low':
|
||||
return 'text-blue-600 bg-blue-50 border-blue-200'
|
||||
default:
|
||||
return 'text-green-600 bg-green-50 border-green-200'
|
||||
}
|
||||
}
|
||||
|
||||
export function getSeverityIcon(severity: string): string {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
return '🚨'
|
||||
case 'high':
|
||||
return '⚠️'
|
||||
case 'medium':
|
||||
return '⚡'
|
||||
case 'low':
|
||||
return 'ℹ️'
|
||||
default:
|
||||
return '✓'
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createLuaEngine, type LuaExecutionContext } from './lua-engine'
|
||||
import { createSandboxedLuaEngine, type SandboxedLuaResult } from './sandboxed-lua-engine'
|
||||
import type { Workflow, WorkflowNode, LuaScript } from './level-types'
|
||||
|
||||
export interface WorkflowExecutionContext {
|
||||
@@ -12,16 +12,19 @@ export interface WorkflowExecutionResult {
|
||||
outputs: Record<string, any>
|
||||
logs: string[]
|
||||
error?: string
|
||||
securityWarnings?: string[]
|
||||
}
|
||||
|
||||
export class WorkflowEngine {
|
||||
private logs: string[] = []
|
||||
private securityWarnings: string[] = []
|
||||
|
||||
async executeWorkflow(
|
||||
workflow: Workflow,
|
||||
context: WorkflowExecutionContext
|
||||
): Promise<WorkflowExecutionResult> {
|
||||
this.logs = []
|
||||
this.securityWarnings = []
|
||||
const outputs: Record<string, any> = {}
|
||||
let currentData = context.data
|
||||
|
||||
@@ -40,6 +43,7 @@ export class WorkflowEngine {
|
||||
outputs,
|
||||
logs: this.logs,
|
||||
error: `Node "${node.label}" failed: ${nodeResult.error}`,
|
||||
securityWarnings: this.securityWarnings,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +62,7 @@ export class WorkflowEngine {
|
||||
success: true,
|
||||
outputs,
|
||||
logs: this.logs,
|
||||
securityWarnings: this.securityWarnings,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
@@ -65,6 +70,7 @@ export class WorkflowEngine {
|
||||
outputs,
|
||||
logs: this.logs,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
securityWarnings: this.securityWarnings,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,27 +170,31 @@ export class WorkflowEngine {
|
||||
data: any,
|
||||
context: WorkflowExecutionContext
|
||||
): Promise<{ success: boolean; output?: any; error?: string }> {
|
||||
const engine = createLuaEngine()
|
||||
const engine = createSandboxedLuaEngine()
|
||||
|
||||
try {
|
||||
const luaContext: LuaExecutionContext = {
|
||||
const luaContext = {
|
||||
data,
|
||||
user: context.user,
|
||||
log: (...args: any[]) => this.log(...args),
|
||||
}
|
||||
|
||||
const result = await engine.execute(code, luaContext)
|
||||
const result: SandboxedLuaResult = await engine.executeWithSandbox(code, luaContext)
|
||||
|
||||
result.logs.forEach((log) => this.log(`[Lua] ${log}`))
|
||||
if (result.security.severity === 'critical' || result.security.severity === 'high') {
|
||||
this.securityWarnings.push(`Security issues detected: ${result.security.issues.map(i => i.message).join(', ')}`)
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
result.execution.logs.forEach((log) => this.log(`[Lua] ${log}`))
|
||||
|
||||
if (!result.execution.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.error,
|
||||
error: result.execution.error,
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, output: result.result }
|
||||
return { success: true, output: result.execution.result }
|
||||
} finally {
|
||||
engine.destroy()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user