Security
js-cgi is designed with sandboxing in mind. By default, scripts have no access to the filesystem, network, or system resources unless explicitly enabled via extensions.
The Sandbox Model
A js-cgi script can only:
- Read the incoming HTTP request (
requestobject) - Write an HTTP response (
responseobject,print()) - Use standard JavaScript built-ins (Math, JSON, Date, etc.)
Everything else requires an extension to be explicitly loaded.
Extension Access Control
Only load the extensions your application needs. A read-only API that returns JSON doesn't need the file or session extensions:
# Minimal for a JSON API
extension = sqlite.so
extension = crypto.so
Production Configuration
# Always disable in production
display_errors = Off
# Log errors to a file instead
error_log = /var/log/js-cgi/error.log
# Limit resource usage
memory_limit = 64M
max_execution_time = 15
File Permissions
The js-cgi process runs as the web server user (typically www-data). Ensure:
- Script files are readable but not writable by
www-data - Data directories (SQLite databases, uploads) are writable only where needed
- The js-cgi binary itself is owned by root and not writable
# Scripts: readable only
sudo chown root:root /var/www/js/*.js
sudo chmod 644 /var/www/js/*.js
# Data directory: writable by web server
sudo chown www-data:www-data /var/www/js/data
sudo chmod 750 /var/www/js/data
SQL Injection Prevention
Always use parameterised queries:
// Safe — parameterised
db.query("SELECT * FROM users WHERE email = ?", [request.query.email]);
// UNSAFE — string concatenation
db.query("SELECT * FROM users WHERE email = '" + request.query.email + "'");
Input Validation
Never trust user input. Validate and sanitise:
const page = parseInt(request.query.page) || 1;
const limit = Math.min(parseInt(request.query.limit) || 20, 100);
if (request.method === "POST") {
const data = JSON.parse(request.body);
if (!data.email || !data.email.includes("@")) {
response.setStatus(400);
print(JSON.stringify({ error: "Invalid email" }));
}
}
Cookie Security
response.setCookie("session", token, {
httpOnly: true, // Not accessible via client JS
secure: true, // HTTPS only
sameSite: "Strict" // No cross-site requests
});
Rate Limiting
For APIs, consider simple rate limiting using SQLite:
const ip = request.headers["x-forwarded-for"] || "unknown";
const db = sqlite.open("/var/www/data/ratelimit.db");
db.exec("CREATE TABLE IF NOT EXISTS requests (ip TEXT, ts INTEGER)");
db.exec("DELETE FROM requests WHERE ts < ?", [Date.now() - 60000]);
db.exec("INSERT INTO requests (ip, ts) VALUES (?, ?)", [ip, Date.now()]);
const count = db.queryOne("SELECT COUNT(*) as c FROM requests WHERE ip = ?", [ip]);
db.close();
if (count.c > 60) {
response.setStatus(429);
print(JSON.stringify({ error: "Too many requests" }));
}