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 (request object)
  • Write an HTTP response (response object, 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" }));
}
Was this page helpful?