CloFix JavaScript (clofix) Scripting Guide
Write clofix JavaScript functions to inspect, filter, and control every HTTP request flowing through CloFix WAF — with full access to request context, shared in-memory state, pattern extraction, and comprehensive OWASP Top 10 protection.
01 Introduction
CloFix WAF executes JavaScript (clofix) scripts on every HTTP request before it reaches your backend. Scripts have read access to a rich request object and read/write access to shared in-memory dictionaries for maintaining state across requests.
Key Concepts
- Scripts live in /etc/clofix/<domain>/js/ — one directory per domain
- All .js files run in filesystem order; first blocking action wins
- Every script must export function clofix(request)
- Scripts return an object with an action property
- Shared dictionaries provide persistent state across requests using sharedDict("name")
- Each script runs in an isolated JavaScript VM with a 2 second wall-clock timeout
- Patterns are automatically extracted from ALL JavaScript files for fast matching
🛡️ OWASP Top 10 Coverage
02 Execution Flow
Every HTTP request passes through a fixed pipeline before reaching your backend.
HTTP Request
│
▼
┌─────────────────────────────────────────────┐
│ WAF Engine — IP extraction, GeoIP, TLS │
│ fingerprint, bot detection │
└───────────────────┬─────────────────────────┘
│ enriched request object
▼
┌─────────────────────────────────────────────┐
│ JavaScript Script Pipeline (per domain) │
│ │
│ 01_rate_limit.js ──► action object │
│ │ allow │
│ 02_bot_detect.js ──► action object │
│ │ allow │
│ 03_sql_injection.js ──► action object │
│ │
│ First non-allow action WINS — pipeline │
│ stops immediately. │
└───────────────────┬─────────────────────────┘
│
┌────────┴────────┐
│ │
block / allow
rate_limit │
│ ▼
▼ Backend / Proxy
Error Response
(403 / 429 / etc)
03 Quick Reference
Request Object — all fields
// ── Core ────────────────────────────────────────────── request.ip // "203.0.113.5" client IP request.method // "GET" / "POST" … request.path // "/page" path only request.url // "/page?id=1" full URI request.host // "example.com" request.query // { id: "1", page: "2" } parsed query // ── Headers & Body ─────────────────────────────────── request.headers // { "user-agent": "...", "accept": "..." } request.body // raw request body string request.cookies // { session_id: "...", csrf: "..." } request.user_agent // "Mozilla/5.0 …" request.referer // "https://google.com/…" request.content_type // "application/json" request.content_length // 1024 (bytes) // ── Security signals ─────────────────────────────────── request.is_tor // true if Tor exit node request.timestamp // 1709123456 Unix timestamp
Return Object — full structure
return { action: "block", // required: "allow", "block", "rate_limit", "redirect" status: 403, // HTTP status (auto-filled if omitted) reason: "SQL Injection detected", // human-readable reason rule_name: "SQLI_001", // rule identifier — appears in WAF logs category: "sql_injection", // attack category confidence: 95, // confidence score (0-100) severity: "high", // "critical", "high", "medium", "low" redirect_to: "", // target URL when action == "redirect" }
04 Directory Structure
JavaScript files must be placed in the domain-specific JS directory:
/etc/clofix/
├── example.com/
│ └── js/
│ ├── 01_rate_limit.js
│ ├── 02_bot_detection.js
│ ├── 03_sql_injection.js
│ └── 04_custom_rules.js
├── api.example.com/
│ └── js/
│ └── api_security.js
└── shop.example.com/
└── js/
└── geo_block.js
05 Domain Configuration
Enable JavaScript scripting in your domain configuration:
domain example.com {
backend http://localhost:8080;
# JavaScript Blocker Configuration
js_blocker {
enabled on;
js_dir /etc/clofix/example.com/js;
scan_interval 5m;
block_threshold 70;
max_file_size 1MB;
enable_logging on;
enable_execution on;
execution_timeout 2s;
include_extensions .js,.mjs,.cjs;
}
}
06 Script Structure
Minimal Script
function clofix(request) { // Your logic here return { action: "allow" }; }
Full Template
// Configuration constants const CONFIG = { ENABLED: true, RATE_LIMIT: 100, BLOCK_TIME: 300 }; // Helper functions function isWhitelisted(ip) { const whitelist = { "192.168.1.1": true }; return whitelist[ip] === true; } // Main entry point — must be named clofix function clofix(request) { if (!CONFIG.ENABLED) { return { action: "allow" }; } const ip = request.ip; if (isWhitelisted(ip)) { return { action: "allow" }; } // ... security logic ... return { action: "allow" }; }
07 Action System
The clofix function must return an object with at minimum an action property.
Supported Action Types
Examples
// Block with full metadata return { action: "block", status: 403, reason: "SQL Injection detected", rule_name: "SQLI_001", category: "sql_injection", confidence: 95 }; // Rate limit return { action: "rate_limit", status: 429, reason: "Too many requests", rule_name: "RL_001" }; // Redirect return { action: "redirect", status: 301, redirect_to: "https://example.com/new-path" };
08 Request Object Reference
| Property | Type | Description | Example |
|---|---|---|---|
| ip | string | Client IP address | "203.0.113.5" |
| method | string | HTTP method | "GET", "POST" |
| path | string | URI path only | "/api/users" |
| url | string | Full URL with query | "/api/users?id=1" |
| host | string | Host header | "example.com" |
| query | object | Parsed query parameters | { id: "1", page: "2" } |
| headers | object | All HTTP headers | { "user-agent": "..." } |
| body | string | Request body | "{\"name\":\"test\"}" |
| cookies | object | All cookies | { session_id: "abc123" } |
| user_agent | string | User-Agent header | "Mozilla/5.0 ..." |
| referer | string | Referer header | "https://google.com" |
| content_type | string | Content-Type header | "application/json" |
| content_length | number | Content length | 1024 |
| is_tor | boolean | Tor exit node | true/false |
| timestamp | number | Unix timestamp | 1709123456 |
10 Rate Limiting
Implement rate limiting using shared dictionaries:
const CONFIG = { REQUESTS_PER_MINUTE: 60, BLOCK_TIME: 300 // 5 minutes }; function clofix(request) { const dict = sharedDict("ddos_attack"); const ip = request.ip; const now = Math.floor(Date.now() / 1000); // Check if currently blocked const blocked = dict.get("block:" + ip) || 0; if (blocked > now) { return { action: "rate_limit", reason: "Temporarily blocked due to rate limit" }; } // FIXED: Use timestamp array for accurate counting const key = "rate:" + ip; // Get existing timestamps let timestamps = dict.get(key); if (!timestamps) { timestamps = []; } else { try { timestamps = JSON.parse(timestamps); } catch (e) { timestamps = []; } } // Remove timestamps older than 60 seconds const cutoff = now - 60; timestamps = timestamps.filter(ts => ts > cutoff); // Check if over limit if (timestamps.length >= CONFIG.REQUESTS_PER_MINUTE) { // Block for 5 minutes dict.set("block:" + ip, now + CONFIG.BLOCK_TIME, CONFIG.BLOCK_TIME); dict.delete(key); return { action: "rate_limit", reason: "Rate limit exceeded", rule_name: "RL_001" }; } // Add current timestamp timestamps.push(now); dict.set(key, JSON.stringify(timestamps), 70); return { action: "allow" }; }
11 Pattern Extraction
The system automatically extracts patterns from ALL JavaScript files, not just executable ones. These patterns are used for fast matching without execution.
// Strings in this file will be extracted as patterns const maliciousPatterns = [ "eval(atob(", "document.write(String.fromCharCode", "coinhive.min.js", "cryptonight.wasm" ]; // Regex patterns will also be extracted const sqlPatterns = [ /union.*select/i, /select.*from/i, /insert.*into/i ]; // Function names become patterns too function detectSQLInjection(input) { // This function name "detectSQLInjection" becomes a pattern return sqlPatterns.some(p => p.test(input)); }
12 Logging
Use the global log object for structured logging:
function clofix(request) { // Simple log messages log.info("Processing request from " + request.ip); log.warn("Suspicious activity detected"); log.error("Script error occurred"); // Structured attack logging log.attack({ rule_name: "SQLI_001", category: "sql_injection", confidence: 95, payload: request.body.substring(0, 100) }); return { action: "allow" }; }
13 Execution Limits
| Limit | Default | Purpose |
|---|---|---|
| Wall-clock timeout | 2 seconds | Script that takes longer is killed and the request is blocked with 503 |
| Memory | 32 MB | Memory limit per script execution |
| File size | 10 MB | Maximum JavaScript file size processed |
14 OWASP Top 10 Examples
SQL Injection (A03:2021)
const SQL_PATTERNS = [ /union.*select/i, /select.*from/i, /insert.*into/i, /update.*set/i, /delete.*from/i, /drop.*table/i, /--/, /#/, /\/\*/, /or\s+1\s*=\s*1/i, /sleep\s*\(/i ]; function clofix(request) { // Check URL for (let pattern of SQL_PATTERNS) { if (pattern.test(request.url)) { return { action: "block", reason: "SQL Injection detected in URL", category: "sql_injection", confidence: 90 }; } } // Check query parameters if (request.query) { for (let [key, value] of Object.entries(request.query)) { for (let pattern of SQL_PATTERNS) { if (pattern.test(value)) { return { action: "block", reason: `SQL Injection in parameter: ${key}`, category: "sql_injection", confidence: 95 }; } } } } return { action: "allow" }; }
Cross-Site Scripting (XSS) (A03:2021)
const XSS_PATTERNS = [ /<script\b[^>]*>.*?<\/script>/i, /javascript:/i, /onerror\s*=/i, /onload\s*=/i, /onclick\s*=/i, /onmouseover\s*=/i, /alert\s*\(/i, /eval\s*\(/i, /document\.cookie/i, /<iframe\b/i, /<img\b[^>]*onerror/i ]; function clofix(request) { const inputs = [ request.url, request.body, ...Object.values(request.query || {}), ...Object.values(request.cookies || {}), ...Object.values(request.headers || {}) ]; for (let input of inputs) { if (typeof input !== 'string') continue; for (let pattern of XSS_PATTERNS) { if (pattern.test(input)) { return { action: "block", reason: "XSS attack detected", category: "xss", confidence: 90 }; } } } return { action: "allow" }; }
Path Traversal (A01:2021)
const TRAVERSAL_PATTERNS = [ /\.\.\//g, /\.\.\\/g, /\%2e\%2e\%2f/gi, /\%2e\%2e\%5c/gi, /etc\/passwd/i, /etc\/shadow/i, /windows\\system32/i, /boot\.ini/i ]; function clofix(request) { // Check URL path if (request.path) { for (let pattern of TRAVERSAL_PATTERNS) { if (pattern.test(request.path)) { return { action: "block", reason: "Path traversal detected", category: "path_traversal", confidence: 95 }; } } } // Check query parameters if (request.query) { for (let [key, value] of Object.entries(request.query)) { for (let pattern of TRAVERSAL_PATTERNS) { if (pattern.test(value)) { return { action: "block", reason: `Path traversal in parameter: ${key}`, category: "path_traversal", confidence: 95 }; } } } } return { action: "allow" }; }
Command Injection (A03:2021)
const CMD_PATTERNS = [ /;\s*(ls|dir|cat|echo|rm|del|cp|mv|ps|kill)/i, /\|\s*(ls|dir|cat|echo|grep)/i, /&&\s*(ls|dir|cat|echo)/i, /`.*`/g, /\$\(.*\)/g, /(ping|nslookup|traceroute|tracert)\s+[0-9.-]+/i, /(wget|curl)\s+http/i, /\/bin\/sh/i, /\/bin\/bash/i ]; function clofix(request) { const inputs = [ request.url, request.body, ...Object.values(request.query || {}), ...Object.values(request.headers || {}) ]; for (let input of inputs) { if (typeof input !== 'string') continue; for (let pattern of CMD_PATTERNS) { if (pattern.test(input)) { return { action: "block", reason: "Command injection detected", category: "command_injection", confidence: 95 }; } } } return { action: "allow" }; }
Server-Side Request Forgery (SSRF) (A10:2021)
const SSRF_PATTERNS = [ /(?:https?|ftp|file):\/\/(?:127\.0\.0\.1|localhost|169\.254\.|192\.168\.|10\.|172\.(?:1[6-9]|2[0-9]|3[01]))/i, /(?:https?|ftp|file):\/\/(?:0\.0\.0\.0|::1|metadata\.google\.internal|169\.254\.169\.254)/i, /(?:https?|ftp|file):\/\/(?:.*\.internal|.*\.local|.*\.localhost)/i, /(?:https?|ftp|file):\/\/(?:.*\.amazonaws\.com\/latest\/meta-data)/i ]; function clofix(request) { // Check URL parameters if (request.query) { for (let [key, value] of Object.entries(request.query)) { if (typeof value === 'string') { for (let pattern of SSRF_PATTERNS) { if (pattern.test(value)) { return { action: "block", reason: `SSRF attack in parameter: ${key}`, category: "ssrf", confidence: 95 }; } } } } } return { action: "allow" }; }
Cross-Site Request Forgery (CSRF) (A07:2021)
const CONFIG = { ALLOWED_ORIGINS: ["https://example.com", "https://www.example.com"], EXEMPT_PATHS: ["/api/webhook", "/api/public"] }; function clofix(request) { const protectedMethods = { "POST": true, "PUT": true, "DELETE": true, "PATCH": true }; if (!protectedMethods[request.method]) { return { action: "allow" }; } for (let path of CONFIG.EXEMPT_PATHS) { if (request.path.startsWith(path)) { return { action: "allow" }; } } const origin = request.headers["origin"] || ""; if (origin) { let allowed = false; for (let o of CONFIG.ALLOWED_ORIGINS) { if (origin === o) { allowed = true; break; } } if (!allowed) { return { action: "block", reason: "CSRF: Invalid Origin", category: "csrf", confidence: 90 }; } } return { action: "allow" }; }
15 Core Security Examples
GeoIP Country Blocking (requires GeoIP implementation)
const BLOCKED_COUNTRIES = { "XX": true, // Replace with actual country codes "YY": true }; function clofix(request) { if (request.country && BLOCKED_COUNTRIES[request.country]) { return { action: "block", reason: `Access from ${request.country} is not allowed`, category: "geo_block", confidence: 100 }; } return { action: "allow" }; }
Bot Detection
const BOT_UA_PATTERNS = [ "bot", "crawler", "spider", "scraper", "wget", "curl", "python", "go-http-client", "headless", "phantomjs", "puppeteer", "selenium", "playwright" ]; function clofix(request) { const ua = (request.user_agent || "").toLowerCase(); if (!ua) { return { action: "challenge", reason: "Missing User-Agent", category: "bot", confidence: 70 }; } for (let pattern of BOT_UA_PATTERNS) { if (ua.includes(pattern)) { return { action: "block", reason: `Bot detected: ${pattern}`, category: "bot", confidence: 85 }; } } return { action: "allow" }; }
Path-Based IP Whitelist
const PROTECTED = { "/admin": true, "/wp-admin": true }; const ALLOWED = { "192.168.1.100": true, "10.0.0.50": true }; function clofix(request) { for (let path in PROTECTED) { if (request.path.startsWith(path)) { if (!ALLOWED[request.ip]) { return { action: "block", reason: "Unauthorized access to protected path", category: "access_control", confidence: 95 }; } } } return { action: "allow" }; }
Brute Force Login Protection
const CONFIG = { LOGIN_PATHS: ["/login", "/wp-login.php", "/api/auth"], MAX_ATTEMPTS: 5, WINDOW: 300, BAN_TIME: 1800 }; function clofix(request) { const dict = sharedDict("ddos_attack"); const ip = request.ip; const now = Math.floor(Date.now() / 1000); let isLogin = false; for (let path of CONFIG.LOGIN_PATHS) { if (request.path.startsWith(path)) { isLogin = true; break; } } if (!isLogin) { return { action: "allow" }; } const banned = dict.get("bf_ban:" + ip) || 0; if (banned > now) { return { action: "rate_limit", reason: "Too many failed login attempts" }; } const key = "bf_cnt:" + ip; let count = dict.get(key) || 0; count = dict.incr(key, 1); dict.set(key, count, CONFIG.WINDOW); if (count >= CONFIG.MAX_ATTEMPTS) { dict.set("bf_ban:" + ip, now + CONFIG.BAN_TIME, CONFIG.BAN_TIME); dict.delete(key); return { action: "rate_limit", reason: "Rate limit exceeded", rule_name: "BF_001" }; } return { action: "allow" }; }
DDoS Protection
const CONFIG = { BURST_LIMIT: 30, SUSTAINED_LIMIT: 200, BAN1: 60, BAN2: 600, BAN3: 86400 }; function escalateBan(ip, now, dict, type) { const offenseKey = "ddos_off:" + ip; let offense = dict.get(offenseKey) || 0; offense = dict.incr(offenseKey, 1); dict.set(offenseKey, offense, 86400); let banTime; if (offense === 1) banTime = CONFIG.BAN1; else if (offense === 2) banTime = CONFIG.BAN2; else banTime = CONFIG.BAN3; dict.set("ddos_ban:" + ip, now + banTime, banTime); return { action: "rate_limit", reason: `DDoS ${type} detected`, rule_name: "DDOS_001" }; } function clofix(request) { const dict = sharedDict("ddos_attack"); const ip = request.ip; const now = Math.floor(Date.now() / 1000); const banned = dict.get("ddos_ban:" + ip) || 0; if (banned > now) { return { action: "rate_limit" }; } const burstKey = "ddos_burst:" + ip; let burst = dict.get(burstKey) || 0; burst = dict.incr(burstKey, 1); dict.set(burstKey, burst, 2); if (burst > CONFIG.BURST_LIMIT) { return escalateBan(ip, now, dict, "burst"); } const minuteKey = "ddos_min:" + ip + ":" + new Date().toISOString().substring(0, 16); let minute = dict.get(minuteKey) || 0; minute = dict.incr(minuteKey, 1); dict.set(minuteKey, minute, 60); if (minute > CONFIG.SUSTAINED_LIMIT) { return escalateBan(ip, now, dict, "sustained"); } return { action: "allow" }; }
HTTP Method Filter
const READ_ONLY_PATHS = ["/blog", "/docs", "/static"]; function clofix(request) { const method = request.method.toUpperCase(); // Block dangerous methods const dangerous = { "TRACE": true, "TRACK": true, "CONNECT": true }; if (dangerous[method]) { return { action: "block", status: 405, reason: "Method not allowed" }; } // Read-only paths should only allow GET/HEAD for (let path of READ_ONLY_PATHS) { if (request.path.startsWith(path)) { if (method !== "GET" && method !== "HEAD") { return { action: "block", status: 405, reason: "Read-only path" }; } } } return { action: "allow" }; }
Tor / VPN Block
function clofix(request) { if (request.is_tor) { return { action: "block", reason: "Tor exit node access denied", category: "tor", confidence: 100 }; } return { action: "allow" }; }
16 More Useful Scripts
Password Protect Any URL
const CONFIG = { SECRET: "change-me-strong-secret", PROTECTED_PATHS: ["/staging", "/preview", "/beta"], TRUSTED_IPS: { "192.168.1.0": true } }; function clofix(request) { let protected = false; for (let p of CONFIG.PROTECTED_PATHS) { if (request.path.startsWith(p)) { protected = true; break; } } if (!protected || CONFIG.TRUSTED_IPS[request.ip]) { return { action: "allow" }; } const token = request.query["token"]; if (token === CONFIG.SECRET) { return { action: "allow" }; } return { action: "block", status: 401, reason: "Authentication required" }; }
API Key Authentication
const API_KEYS = { "key-client-alpha-1234": { name: "client-alpha", rate: 100 }, "key-client-beta-5678": { name: "client-beta", rate: 500 } }; const PROTECTED = ["/api/", "/v1/", "/v2/"]; function clofix(request) { let needs = false; for (let p of PROTECTED) { if (request.path.startsWith(p)) { needs = true; break; } } if (!needs) return { action: "allow" }; const key = request.headers["x-api-key"] || request.query["api_key"]; if (!key) { return { action: "block", status: 401, reason: "API key required" }; } if (!API_KEYS[key]) { return { action: "block", status: 403, reason: "Invalid API key" }; } return { action: "allow" }; }
Dangerous File Type Filter
const BLOCKED_EXT = [ ".php", ".php3", ".php5", ".phtml", ".asp", ".aspx", ".jsp", ".cgi", ".pl", ".py", ".sh", ".exe", ".bat", ".jar" ]; function clofix(request) { const path = request.path.toLowerCase(); for (let ext of BLOCKED_EXT) { if (path.endsWith(ext)) { return { action: "block", reason: `File extension ${ext} not allowed`, category: "file_filter", confidence: 95 }; } } return { action: "allow" }; }
Honeypot Bot Trap
const CONFIG = { BAN_TIME: 86400, TRAP_PATHS: ["/trap-link", "/do-not-visit", "/honeypot"], TRUSTED_AGENTS: ["googlebot", "bingbot"] }; function clofix(request) { const dict = sharedDict("ddos_attack"); const ip = request.ip; const now = Math.floor(Date.now() / 1000); const path = request.path.toLowerCase(); const ua = (request.user_agent || "").toLowerCase(); for (let trap of CONFIG.TRAP_PATHS) { if (path === trap) { let trusted = false; for (let a of CONFIG.TRUSTED_AGENTS) { if (ua.includes(a)) trusted = true; } if (!trusted) { dict.set("hp_ban:" + ip, now + CONFIG.BAN_TIME, CONFIG.BAN_TIME); return { action: "block", reason: "Honeypot triggered" }; } } } return { action: "allow" }; }
Maintenance Mode
const CONFIG = { ENABLED: true, ALLOWED_IPS: { "203.0.113.10": true }, ALWAYS_ON: ["/health", "/ping", "/status"] }; function clofix(request) { if (!CONFIG.ENABLED) return { action: "allow" }; for (let p of CONFIG.ALWAYS_ON) { if (request.path === p) return { action: "allow" }; } if (CONFIG.ALLOWED_IPS[request.ip]) { return { action: "allow" }; } return { action: "block", status: 503, reason: "Under maintenance" }; }
Smart Redirects
const EXACT = { "/home": { to: "/", code: 301 }, "/about-us": { to: "/about", code: 301 } }; const PREFIX = [ { from: "/old-blog/", to: "/blog/", code: 301 } ]; function clofix(request) { const path = request.path; if (EXACT[path]) { return { action: "redirect", status: EXACT[path].code, redirect_to: EXACT[path].to }; } for (let r of PREFIX) { if (path.startsWith(r.from)) { const dest = r.to + path.substring(r.from.length); return { action: "redirect", status: r.code, redirect_to: dest }; } } return { action: "allow" }; }
17 Testing
Diagnostic Script
function clofix(request) { log.info("=== DEBUG ==="); log.info("IP: " + request.ip); log.info("Method: " + request.method); log.info("Path: " + request.path); log.info("UA: " + request.user_agent); log.info("Is Tor: " + request.is_tor); return { action: "allow" }; }
Shell Commands
# Watch WAF logs sudo tail -f /var/log/clofix/waf.log | grep -i '\[JSBlocker\]' # Test normal request curl https://your-domain.com/ # Test SQL injection curl 'https://your-domain.com/?id=1%20UNION%20SELECT%20*%20FROM%20users' # Test XSS curl 'https://your-domain.com/?q=<script>alert(1)</script>' # Test rate limiting for i in $(seq 1 200); do curl -s -o /dev/null https://your-domain.com/ & done wait # Test scanner detection curl -A "sqlmap/1.7" https://your-domain.com/
18 Troubleshooting
| Error / Symptom | Cause | Fix |
|---|---|---|
| function clofix not found | Function missing or misnamed | Export exactly function clofix(request) |
| sharedDict is not defined | Shared dict not declared in config | Add lua_shared_dict <name> <size>m; to config |
| Cannot read property 'ip' of undefined | Request object is undefined | Function signature must be clofix(request) |
| JavaScript execution timed out | Script exceeds 2 seconds | Optimize loops, use shared dict for caching |
| SyntaxError: Unexpected token | JavaScript syntax error | Validate with node -c script.js |
| Changes not taking effect | File not saved or scanned | Check file permissions, trigger rescan via API |
19 Best Practices
- One script per concern — keep
rate_limit.js,bot_detection.js,sql_injection.jsseparate - Always validate inputs — use
if (request.body && typeof request.body === 'string') - Return early — check blocklists first before heavy pattern matching
- Use shared dictionaries with TTL — always set expiration times to prevent memory leaks
- Log attacks with context — use
log.attack()for structured logging - Test with diagnostic script first — deploy debug.js to verify your environment
- Numeric prefixes on filenames — use
01_rate_limit.js,02_bot_detect.jsto control order - CONFIG objects at the top — put all tunable values in a CONFIG object for easy adjustments
- Remove debug logs in production — delete or comment out
log.info()calls
