Executing User-Generated Code Safely β
Allowing users to create and execute JavaScript code inside your application can unlock powerful customization and automation capabilities. However, it also introduces security risks, performance concerns, and sandboxing challenges.
The wrong approach (such as using JavaScriptβs built-in eval
) can expose your system to serious vulnerabilities, including infinite loops, crashes, and unauthorized access to sensitive data.
In this article, we'll walk through a safe and efficient way to execute user-generated JavaScript code using QuickJS, a lightweight JavaScript engine that enables sandboxed execution.
π¨ Why Not Use eval
? β
Using eval
for user-generated code may seem like the easiest solution, but it's also the most dangerous:
β Security Risks β Users could execute malicious code, access system resources, or modify global variables.
β Performance Issues β JavaScript is single-threaded; if a user writes an infinite loop, it will block execution entirely.
β Lack of Isolation β eval
runs inside your main thread, meaning any bug or infinite loop can crash your entire application.
A better solution is to run user-generated code inside a secure, sandboxed environment where it cannot interfere with the host system.
π Use Case: JSON Log Processing β
Letβs consider a real-world scenario:
β
We have a JSON log file that records system events.
β
Users should be able to write their own logic to decide whether an alert should be triggered.
β
The code should be executed safely, without the risk of interfering with the host system.
β
Users should be able to write TypeScript, benefiting from autocomplete, type checking, and code suggestions.
We'll use QuickJS to sandbox and safely execute the userβs logic.
π§ Setting Up the Sandbox β
1οΈβ£ Project Structure β
We'll structure our project as follows:
|- log.jsonl # JSON logs (one entry per line)
|- src
|- custom.ts # User-created code
|- types.ts # Type definitions
|- index.ts # Main execution file
2οΈβ£ Example Data (Log File & Type Definitions) β
π log.jsonl
β
{"message":"some log message","errorCode":0,"dateTime":"2025-02-26T07:35:10Z"}
{"message":"an error message","errorCode":1,"dateTime":"2025-02-26T07:40:00Z"}
π src/types.ts
(Defining Log Format & Alert Function) β
export type LogRow = {
message: string
errorCode: number
dateTime: string
}
export type AlertDecisionFn = (input: LogRow[]) => boolean
π src/index.ts
(Main Execution) β
import { readFileSync } from 'node:fs'
import { shouldAlert } from './custom.js'
import type { LogRow } from './types.js'
const main = () => {
const logFileContent = readFileSync('log.jsonl', 'utf-8')
const logs: LogRow[] = logFileContent.split('\n').map(line => JSON.parse(line))
return shouldAlert(logs)
}
export default main()
π¨ src/custom.ts
(User-Written Code) β
import type { AlertDecisionFn } from './types.js'
export const shouldAlert: AlertDecisionFn = (input) => {
// User-defined logic
// return booleanResult
}
βοΈ Mounting Files in the Sandbox β
We'll map these files into a QuickJS sandbox, allowing users to edit only custom.ts
, while keeping the rest of the logic untouched.
import { type SandboxOptions } from '../../src/index.js'
const userGeneratedCode = 'return true' // Example user input
const logFileContent = `{"message":"some log message","errorCode":0,"dateTime":"2025-02-26T07:35:10Z"}
{"message":"an error message","errorCode":1,"dateTime":"2025-02-26T07:40:00Z"}`
const options: SandboxOptions = {
allowFetch: false,
allowFs: true,
transformTypescript: true,
mountFs: {
'log.jsonl': logFileContent,
src: {
'types.ts': `export type LogRow = {
message: string
errorCode: number
dateTime: string
}
export type AlertDecisionFn = ( input: LogRow[] ) => boolean`,
'custom.ts': `import type { AlertDecisionFn } from './types.js'
export const shouldAlert: AlertDecisionFn = (input) => {
${userGeneratedCode}
}`,
},
},
}
πΉ This setup mounts the JSON log data and the userβs code while keeping the execution logic intact.
π Running the User's Code β
Hereβs how the sandboxed execution works:
import { type SandboxOptions, loadQuickJs } from '../../src/index.js'
const { runSandboxed } = await loadQuickJs()
const executionCode = `import { readFileSync } from 'node:fs'
import { shouldAlert } from './custom.js'
import type { LogRow } from './types.js'
const main = () => {
const logFileContent = readFileSync('log.jsonl', 'utf-8')
const logs: LogRow[] = logFileContent.split('\\n').map(line => JSON.parse(line))
return shouldAlert(logs)
}
export default main()
`
const resultSandbox = await runSandboxed(async ({ evalCode }) => {
return await evalCode(executionCode, undefined, options)
}, options)
console.log(resultSandbox)
// Output:
// { ok: true, data: true }
β The userβs logic is executed safely, and their function determines whether an alert should be triggered.
ποΈ Adding Memory (State Management) β
To improve functionality, we can add persistent memory. This allows the function to track previous alerts, implementing debouncing or rate limiting.
Since QuickJS functions are stateless, the host system should manage state.
πΎ Storing State in Memory β
Weβll store the last alert time in the host and expose functions for reading and updating it.
let memory: Date = new Date(0) // Store last alert timestamp
const options: SandboxOptions = {
allowFetch: false,
allowFs: true,
transformTypescript: true,
env: {
setLastAlert: (input: Date) => {
console.log('Setting last alert:', input)
memory = input
},
getLastAlert: () => memory,
},
}
π οΈ Accessing State from the Guest β
Inside the sandbox, the user can now call env.getLastAlert()
and env.setLastAlert(new Date())
to manage state.
π― Key Takeaways β
β
QuickJS provides a safe, isolated sandbox for executing user-generated JavaScript code.
β
No risk of crashes, infinite loops, or unauthorized system access.
β
TypeScript support allows users to write code with better tooling (autocomplete, suggestions).
β
Custom memory handling can be used for debouncing or rate limiting logic.
With this setup, user-generated JavaScript execution becomes secure, efficient, and highly customizable β without compromising system stability. π
π Next Steps β
π Try it out: Implement QuickJS in your own project.
π Enhance security: Add execution timeouts and memory limits.
π Expand functionality: Allow users to fetch external data securely.
QuickJS makes sandboxing user-generated code easy and secure β without the headaches of eval
. π―