CloFix Lua Scripting Guide
Write clofix Lua scripts to inspect, filter, and control every HTTP request flowing through CloFix WAF — with full access to request context, threat intelligence, built-in clofix.* helpers, and shared cross-request state.
01 Introduction
CloFix WAF executes Lua scripts on every HTTP request before it reaches your backend. Scripts have read access to a rich request object, a full clofix.* helper API, a utils.* string library, and read/write access to shared in-memory dictionaries for maintaining state across requests.
Key Concepts
- Scripts live in ./lua_script/<domain>/ — one directory per domain
- All .lua files run in filesystem order; first blocking action wins
- Every script must define function clofix_main(request)
- Scripts return a table with an action field, or a legacy bool for backwards compatibility
- The clofix.* global provides helpers: decoding, rate limiting, threat intel, logging
- The utils.* global provides string, JSON, and timing utilities
- Each script runs in an isolated Lua state with a 50 ms wall-clock timeout
🛡️ OWASP Top 10 Coverage
🔒 Comprehensive Protection Matrix — What You Can Build with Lua
This is just a sample — with CloFix Lua you can build any security logic imaginable using the full power of a programming language combined with real-time request context and shared state.
02 Execution Flow
Every HTTP request passes through a fixed pipeline before reaching your backend. Understanding this order helps you write efficient, correct scripts.
HTTP Request
│
▼
┌─────────────────────────────────────────────┐
│ WAF Engine — IP extraction, GeoIP, TLS │
│ fingerprint (JA3/JA4), bot score, │
│ is_tor / is_vpn / ip_reputation │
└───────────────────┬─────────────────────────┘
│ enriched request object
▼
┌─────────────────────────────────────────────┐
│ Lua Script Pipeline (per domain) │
│ │
│ 01_fast_checks.lua ──► action table │
│ │ allow │
│ 02_attack_detect.lua ──► action table │
│ │ allow │
│ 03_custom_rules.lua ──► action table │
│ │
│ First non-allow action WINS — pipeline │
│ stops immediately. │
└───────────────────┬─────────────────────────┘
│
┌────────┴────────┐
│ │
block / allow
rate_limit │
│ ▼
│ Backend / Proxy
▼
Error Response
(403 / 429 / etc)
Inside a single script
clofix_main(request)
│
├── 1. Threat Intel fast-path
│ clofix.is_tor() / clofix.ip_reputation()
│ → immediate block if malicious
│
├── 2. Payload Normalization
│ clofix.normalize_payload(request.body)
│ multi-pass URL + HTML decode → lowercase
│
├── 3. Pattern Matching
│ SQL injection / XSS / CMDi patterns
│ on normalized surface
│
├── 4. Rate Limiting
│ clofix.rate_limit_ip(limit, window)
│
├── 5. Threat Score Accumulation
│ score = score + N per matched signal
│
└── 6. Decision
score >= 80 → block
score >= 50 → challenge
score >= 20 → log_only
score < 20 → allow
03 Quick Reference
Everything you need on one page — request fields, return table, and built-in functions. Full details in later sections.
Request Object — all fields at a glance
-- ── Core ────────────────────────────────────────────── request.ip -- "203.0.113.5" client IP request.method -- "GET" / "POST" … request.scheme -- "https" request.host -- "example.com" request.port -- 443 request.uri -- "/page?id=1" full URI request.path -- "/page" path only request.raw_query -- "id=1&q=test" unparsed query -- ── Body ────────────────────────────────────────────── request.body -- raw request body string request.raw_body -- binary-safe body bytes request.json_body -- JSON-encoded body (if Content-Type: application/json) -- ── Tables ──────────────────────────────────────────── request.headers -- { ["user-agent"] = "…", … } lowercase keys request.raw_headers -- full header block as string request.cookies -- { ["session_id"] = "…", … } request.query -- { ["id"] = "1", … } parsed query params request.form_data -- { ["username"] = "…", … } POST form fields -- ── Client metadata ──────────────────────────────────── request.user_agent -- "Mozilla/5.0 …" request.referer -- "https://google.com/…" request.content_type -- "application/json" request.content_length -- 1024 (bytes) request.is_tls -- true / false request.tls_version -- "TLS 1.3" -- ── GeoIP ────────────────────────────────────────────── request.country -- "US" / "BD" / "CN" ISO-3166 request.region -- "California" request.city -- "San Francisco" request.asn -- "AS15169" request.org -- "Google LLC" -- ── Security fingerprint ─────────────────────────────── request.ja3 -- JA3 TLS fingerprint hash request.ja4 -- JA4 TLS fingerprint hash request.is_tor -- true if Tor exit node request.is_vpn -- true if VPN / proxy request.is_bot -- true if bot detected by WAF engine request.is_headless -- true if headless browser request.bot_score -- 0–100 (higher = more likely bot) request.anomaly_score -- cumulative WAF anomaly score request.ip_reputation -- "clean" / "suspicious" / "malicious"
Return Table — full structure
return { action = "block", -- required: allow / block / challenge / -- rate_limit / log_only / redirect / captcha status = 403, -- HTTP status (auto-filled if omitted) message = "SQL Injection detected", -- human-readable reason rule_name = "SQLI_001", -- rule identifier — appears in WAF logs attack_type = "SQL Injection", -- attack category score = 80, -- threat score (used by scoring engine) log = true, -- force emit a log entry redirect_to = "", -- target URL when action == "redirect" }
Built-in Functions — clofix.*
-- ── Payload decoding ─────────────────────────────────── clofix.url_decode(s) -- URL-decode (%2F → /) clofix.base64_decode(s) -- Base64 decode (standard + URL-safe) clofix.hex_decode(s) -- Hex decode (41 → A) clofix.html_decode(s) -- HTML entity decode (& → &) clofix.normalize_payload(s) -- multi-pass decode + lowercase + collapse whitespace -- USE THIS before all pattern matching -- ── Client info ──────────────────────────────────────── clofix.get_ip() -- current client IP string clofix.get_country() -- ISO country code clofix.get_asn() -- ASN string clofix.get_ja3() -- JA3 fingerprint hash clofix.get_ja4() -- JA4 fingerprint hash clofix.get_bot_score() -- 0–100 bot confidence score -- ── Threat intelligence ──────────────────────────────── clofix.is_tor() -- boolean — Tor exit node? clofix.is_vpn() -- boolean — VPN / proxy? clofix.is_proxy() -- boolean — proxy headers detected? clofix.ip_reputation() -- "clean" / "suspicious" / "malicious" -- ── Rate limiting ────────────────────────────────────── clofix.rate_limit(key, limit, window_secs) -- custom key clofix.rate_limit_ip(limit, window_secs) -- per client IP clofix.rate_limit_endpoint(limit, window_secs) -- per IP + path -- all three return: count, exceeded (boolean) -- ── Logging ──────────────────────────────────────────── clofix.log(level, msg) -- level: "info" / "warn" / "error" clofix.log_attack(rule, type, payload?) -- structured attack log — IP, country, UA, -- method, URI auto-included -- ── Shared dict ─────────────────────────────────────── clofix.shared.ddos_attack -- access by name (declared in config) clofix.shared_dict("name") -- programmatic lookup
Built-in Functions — utils.*
utils.contains(s, sub) -- boolean — plain match utils.starts_with(s, pfx) -- boolean utils.ends_with(s, sfx) -- boolean utils.to_lower(s) -- string utils.to_upper(s) -- string utils.trim(s) -- strip whitespace utils.split(s, sep) -- table / array utils.url_encode(s) -- string utils.url_decode(s) -- string, err? utils.base64_encode(s) -- string utils.base64_decode(s) -- string, err? utils.json_encode(v) -- string, err? utils.json_decode(s) -- value, err? utils.build_query(t) -- "k=v&k2=v2" utils.log(msg) -- INFO log shorthand utils.now() -- Unix timestamp (same as os.time())
Three starter scripts
| File | Purpose | Shared Dict needed |
|---|---|---|
| example_security.lua | Multi-rule scoring engine covering SQLi, XSS, CMDi, SSRF, rate limit, bad UA, Tor, reputation | ddos_attack |
| wordpress_shield.lua | XML-RPC block, author enum, PHP upload guard, wp-login brute force | ddos_attack |
| api_protection.lua | API key auth + per-key quota + endpoint rate limiting | rate_limit |
04 Directory Structure
CloFix creates the domain directory automatically when Lua is enabled. You can also create it manually:
./lua_script/
├── example.com/
│ ├── 01_rate_limit.lua
│ ├── 02_bot_detection.lua
│ └── 03_custom_rules.lua
├── api.example.com/
│ └── api_security.lua
└── shop.example.com/
└── geo_block.lua
05 Domain Configuration
Enable Lua and declare shared dictionaries inside your domain config or via the CloFix Config Manager dashboard:
# ./conf/example.com.conf domain example.com { proxy_pass http://localhost:8080; lua { enabled on; lua_shared_dict ddos_attack 10m; lua_shared_dict rate_limit 5m; lua_shared_dict bot_tracking 2m; } }
06 Script Structure
Minimal Script
function clofix_main(request) -- Your logic here return { action = "allow", status = 200 } end
Full Template
-- ① Config constants (tune without touching logic) local CONFIG = { ENABLED = true, RATE_LIMIT = 100, BLOCK_TIME = 300, } -- ② Helper functions local function is_whitelisted(ip) local wl = { ["192.168.1.1"] = true } return wl[ip] == true end -- ③ Entry point — must be named clofix_main function clofix_main(request) if not CONFIG.ENABLED then return { action = "allow", status = 200 } end local ip = request.ip or request.remote_ip if is_whitelisted(ip) then return { action = "allow", status = 200 } end -- ... security logic ... return { action = "allow", status = 200 } end
07 Action System
clofix_main must return a table with at minimum an action field. The WAF engine reads all fields to decide how to respond, what to log, and whether to alert.
Return Table Fields
| Field | Type | Required | Description |
|---|---|---|---|
| action | string | ✓ | One of the action types below |
| status | number | — | HTTP status code. Auto-filled if omitted (403 for block, 429 for rate_limit, etc.) |
| message | string | — | Human-readable reason. Logged and included in alerts. |
| rule_name | string | — | Rule identifier e.g. SQLI_001. Appears in WAF logs. |
| attack_type | string | — | Attack category e.g. SQL Injection. |
| log | boolean | — | Force-emit a log entry. Automatically true for blocking actions. |
| redirect_to | string | — | Target URL when action is redirect. |
| score | number | — | Used by the scoring engine. If action is omitted, score decides the action. |
Supported Action Types
Examples
-- Block with full metadata return { action = "block", status = 403, message = "SQL Injection detected", rule_name = "SQLI_001", attack_type = "SQL Injection", log = true, } -- Rate limit return { action = "rate_limit", status = 429, message = "Too many requests", rule_name = "RL_001", attack_type = "Rate Limit", log = true, } -- Redirect return { action = "redirect", status = 301, redirect_to = "https://example.com/new-path", } -- Allow (minimal) return { action = "allow" }
08 Threat Scoring Engine
Instead of returning a fixed action, scripts can accumulate a numeric score across multiple detection checks and let the engine decide the action threshold. This is ideal for multi-rule scripts.
function clofix_main(request) local score = 0 -- Each detection adds to the score if request.is_tor then score = score + 50 end if request.is_bot then score = score + 30 end if request.bot_score > 70 then score = score + 40 end local ua = utils.to_lower(request.user_agent or "") if utils.contains(ua, "sqlmap") then score = score + 80 end -- Return score — engine applies the threshold table above return { score = score, rule_name = "MULTI_001", attack_type = "Combined Threat", log = score >= 20, } end
If both action and score are returned, action takes precedence unless it is "allow" and score ≥ 20, in which case the score upgrades the action.
09 Request Object Reference
All fields on the request table are read-only. Always guard against nil using or "default".
Core Fields
| Field | Type | Description | Example |
|---|---|---|---|
| ip | string | Client IP address (alias of remote_ip) | "203.0.113.5" |
| method | string | HTTP method | "GET", "POST" |
| scheme | string | Protocol scheme | "https" |
| host | string | Host header value | "example.com" |
| hostname | string | Hostname without port | "example.com" |
| port | number | Client source port | 54321 |
| uri | string | Full URI with query string | "/page?id=1" |
| path | string | URI path only | "/page" |
| raw_query | string | Raw query string (unparsed) | "id=1&q=test" |
| raw_url | string | Full raw URL | "https://example.com/page?id=1" |
| protocol | string | HTTP version | "HTTP/1.1" |
| remote_ip | string | Client IP address | "203.0.113.5" |
| remote_addr | string | IP:port combined | "203.0.113.5:54321" |
| timestamp | number | Unix timestamp of request | 1709123456 |
| request_id | string | Unique request ID | "abc123…" |
Body Fields
| Field | Type | Description |
|---|---|---|
| body | string | Request body as UTF-8 string |
| raw_body | string | Raw body bytes (same as body, binary-safe) |
| json_body | string | JSON-encoded body if Content-Type is application/json |
| form_data | table | Parsed POST form fields |
Table Fields — headers, cookies, query
-- Headers (keys are lowercase) local ua = request.headers["user-agent"] local auth = request.headers["authorization"] local raw = request.raw_headers -- full header block as string -- Cookies local sess = request.cookies["session_id"] -- Query string (parsed) local id = request.query["id"] -- Query string (raw, for injection scanning) local rq = request.raw_query -- POST form fields local user = request.form_data["username"]
Client Metadata
| Field | Type | Description |
|---|---|---|
| user_agent | string | User-Agent header |
| referer | string | Referer header |
| content_type | string | Content-Type header |
| content_length | number | Content-Length header value |
| origin_header | string | Origin header |
| x_forwarded_for | string | X-Forwarded-For header |
| is_tls | boolean | true if HTTPS connection |
| tls_version | string | TLS version string — "TLS 1.3" |
| tls_cipher | string | TLS cipher suite |
GeoIP Fields
| Field | Type | Description |
|---|---|---|
| country | string | ISO-3166 country code — "US", "BD", "CN" |
| region | string | Region / state name |
| city | string | City name |
| asn | string | Autonomous System Number — "AS12345" |
| org | string | ISP or organisation name |
Security & Fingerprint Fields
| Field | Type | Description |
|---|---|---|
| ja3 | string | JA3 TLS fingerprint hash |
| ja4 | string | JA4 TLS fingerprint hash |
| is_tor | boolean | true if Tor exit node |
| is_vpn | boolean | true if VPN / proxy detected |
| is_cloud_provider | boolean | true if AWS / GCP / Azure IP |
| is_bot | boolean | true if WAF bot detection triggered |
| is_headless | boolean | true if headless browser detected |
| bot_score | number | 0–100 bot confidence score (higher = more likely bot) |
| anomaly_score | number | Cumulative WAF anomaly score |
| ip_reputation | string | "clean" | "suspicious" | "malicious" |
10 clofix.* Helper API
The clofix global table is available inside every script. It provides payload decoding, threat intelligence, rate limiting, and logging — all bound to the current request context.
Payload Decoding
| Function | Returns | Description |
|---|---|---|
| clofix.url_decode(s) | string, err? | URL-decode a string (%XX → char) |
| clofix.base64_decode(s) | string, err? | Base64 decode (standard + URL-safe) |
| clofix.hex_decode(s) | string, err? | Hex-decode a string |
| clofix.html_decode(s) | string | HTML entity decode (& → &) |
| clofix.normalize_payload(s) | string | Multi-pass decode + lowercase + collapse whitespace |
Client & Fingerprint Info
| Function | Returns | Description |
|---|---|---|
| clofix.get_ip() | string | Current client IP |
| clofix.get_country() | string | ISO country code of client |
| clofix.get_asn() | string | ASN of client |
| clofix.get_ja3() | string | JA3 fingerprint hash |
| clofix.get_ja4() | string | JA4 fingerprint hash |
| clofix.get_bot_score() | number | 0–100 bot confidence score |
Threat Intelligence
| Function | Returns | Description |
|---|---|---|
| clofix.is_tor() | boolean | true if client is a Tor exit node |
| clofix.is_vpn() | boolean | true if client is a VPN / proxy |
| clofix.is_proxy() | boolean | true if proxy headers detected (Via, X-Forwarded-For, Forwarded) |
| clofix.ip_reputation() | string | "clean" | "suspicious" | "malicious" |
Rate Limiting
| Function | Signature | Returns | Description |
|---|---|---|---|
| clofix.rate_limit | (key, limit, window_secs) | count, exceeded | Generic rate limiter. key can be any string. |
| clofix.rate_limit_ip | (limit, window_secs) | count, exceeded | Per-IP rate limit. Key is ip:<ip>. |
| clofix.rate_limit_endpoint | (limit, window_secs) | count, exceeded | Per-IP per-path rate limit. Key is ep:<ip>:<path>. |
Logging
| Function | Description |
|---|---|
| clofix.log(level, msg) | Log a message. level: "info", "warn", "error" |
| clofix.log_attack(rule, type, payload?) | Structured attack log with full request context auto-included |
Shared Dict Bridge
| Function | Returns | Description |
|---|---|---|
| clofix.shared_dict(name) | dict | nil, err | Get a shared dictionary by name (same as clofix.shared) |
function clofix_main(request) -- Threat intelligence fast-path if clofix.is_tor() then return { action="block", status=403, rule_name="TI_001", attack_type="Tor", log=true } end if clofix.ip_reputation() == "malicious" then return { action="block", status=403, rule_name="TI_002", attack_type="Reputation", log=true } end -- Normalize body for injection scanning local norm = clofix.normalize_payload(request.body or "") -- Per-IP rate limiting: 100 req / 60s local cnt, exceeded = clofix.rate_limit_ip(100, 60) if exceeded then clofix.log_attack("RL_001", "Rate Limit") return { action="rate_limit", status=429, rule_name="RL_001", log=true } end -- Base64-decode a suspicious header local decoded = clofix.base64_decode(request.headers["x-payload"] or "") return { action="allow" } end
11 utils.* Helper API
The utils global provides general-purpose string, JSON, and timing utilities available in every script.
| Function | Returns | Description |
|---|---|---|
| utils.url_encode(s) | string | URL-encode a string |
| utils.url_decode(s) | string, err? | URL-decode a string |
| utils.base64_encode(s) | string | Base64 encode |
| utils.base64_decode(s) | string, err? | Base64 decode |
| utils.json_encode(v) | string, err? | Encode Lua value to JSON string |
| utils.json_decode(s) | value, err? | Decode JSON string to Lua value |
| utils.build_query(t) | string | Build a URL query string from a table |
| utils.contains(s, sub) | boolean | true if s contains sub (plain match) |
| utils.starts_with(s, pfx) | boolean | true if s starts with pfx |
| utils.ends_with(s, sfx) | boolean | true if s ends with sfx |
| utils.to_lower(s) | string | Lowercase a string |
| utils.to_upper(s) | string | Uppercase a string |
| utils.trim(s) | string | Trim leading/trailing whitespace |
| utils.split(s, sep) | table | Split string by separator into array |
| utils.log(msg) | — | Log a message at INFO level |
| utils.now() | number | Current Unix timestamp |
local ua = utils.to_lower(request.user_agent or "") local bad = utils.contains(ua, "sqlmap") local parts = utils.split(request.path, "/") -- { "", "api", "users" } local data = utils.json_decode(request.body or "{}") if data and data.username then utils.log("login attempt: " .. tostring(data.username)) end local ts = utils.now() -- same as os.time()
12 Shared Dictionaries
Shared dictionaries maintain state across requests and scripts for a domain. They are thread-safe and persist for the lifetime of the WAF process.
Access
-- Via clofix.shared (standard) local dict = clofix.shared.ddos_attack -- Via clofix bridge local dict = clofix.shared_dict("ddos_attack") -- Via global functions (no userdata — string-based) shared_dict_set("ddos_attack", "key", "value") shared_dict_get("ddos_attack", "key") shared_dict_incr("ddos_attack", "counter", 1) shared_dict_set_ttl("ddos_attack", "key", "val", 60)
Methods (clofix.shared API)
| Method | Signature | Returns | Description |
|---|---|---|---|
| get | dict:get(key) | value | nil | Read value; nil if missing or expired |
| set | dict:set(key, value, ttl?) | — | Store value with optional TTL in seconds |
| incr | dict:incr(key, delta) | new number | Atomically increment a numeric counter |
| delete | dict:delete(key) | — | Remove a key immediately |
| flush_all | dict:flush_all() | — | Clear all keys in the dictionary |
| ttl | dict:ttl(key) | seconds | Remaining TTL (0 if no expiry set) |
13 Rate Limiting
Two approaches are available: the built-in clofix.rate_limit* functions (backed by a persistent in-memory store), or manual shared-dict counters for full control.
function clofix_main(request) -- Per-IP: 60 requests per minute local cnt, exceeded = clofix.rate_limit_ip(60, 60) if exceeded then return { action="rate_limit", status=429, rule_name="RL_IP", log=true } end -- Per-IP per-endpoint: 10 requests per minute on /login if utils.starts_with(request.path, "/login") then local ecnt, eex = clofix.rate_limit_endpoint(10, 60) if eex then return { action="rate_limit", status=429, rule_name="RL_LOGIN", log=true } end end -- Custom key (per API key per day) local akey = request.headers["x-api-key"] or "anon" local kkey = "apikey:" .. akey .. ":" .. os.date("%Y%m%d") local kcnt, kex = clofix.rate_limit(kkey, 10000, 86400) if kex then return { action="rate_limit", status=429, rule_name="RL_QUOTA", log=true } end return { action="allow" } end
14 Logging
| Method | Level | Use for |
|---|---|---|
| clofix.log("info", msg) | INFO | Normal informational events |
| clofix.log("warn", msg) | WARN | Blocked requests, suspicious activity |
| clofix.log("error", msg) | ERROR | Script errors, unexpected conditions |
| clofix.log_attack(rule, type, payload?) | WARN | Structured attack log with full request context auto-included |
| utils.log(msg) | INFO | Simple one-arg info log shorthand |
-- Structured attack log — IP, country, UA, method, URI auto-included clofix.log_attack("SQLI_001", "SQL Injection", request.raw_query) -- General log clofix.log("warn", string.format( "[BLOCKED] IP=%s Path=%s Score=%d", request.ip, request.path, 85 )) -- Shorthand info log utils.log("Processing request from: " .. (request.ip or "unknown"))
15 Execution Limits
Each script runs in an isolated Lua state with hard resource limits to prevent runaway scripts from impacting WAF throughput.
| Limit | Default | Purpose |
|---|---|---|
| Wall-clock timeout | 50 ms | Script that takes longer is killed and the request is blocked with 503 |
| Call stack depth | 200 frames | Prevents infinite recursion / stack overflow |
| Memory | 32 MB | Lua allocator limit per script execution |
16 Core Examples
Rate Limiter
local CONFIG = { REQUESTS_PER_MINUTE = 60, BLOCK_TIME = 300, } function clofix_main(request) local ip = request.ip local dict = clofix.shared.ddos_attack local now = os.time() local blocked_until = dict:get("block:" .. ip) or 0 if blocked_until > now then return { action="rate_limit", status=429, rule_name="RL_001", log=true } end local key = "cnt:" .. ip .. ":" .. os.date("%Y%m%d%H%M") local count = dict:incr(key, 1) dict:set(key, count, 60) if count > CONFIG.REQUESTS_PER_MINUTE then dict:set("block:" .. ip, now + CONFIG.BLOCK_TIME, CONFIG.BLOCK_TIME) clofix.log_attack("RL_001", "Rate Limit") return { action="rate_limit", status=429, rule_name="RL_001", log=true } end return { action="allow" } end
Geo / Country Block
local BLOCKED = { ["XX"]=true, ["YY"]=true } function clofix_main(request) local cc = request.country if cc and BLOCKED[cc] then clofix.log_attack("GEO_001", "Geo Block") return { action="block", status=403, rule_name="GEO_001", attack_type="Geo Block", log=true } end return { action="allow" } end
Bot Detection
local BAD_UA = { "python", "curl", "wget", "scrapy", "bot", "crawler", "spider", "scanner" } local function is_bad_ua(ua) if not ua or ua == "" then return true end local low = utils.to_lower(ua) for _, p in ipairs(BAD_UA) do if utils.contains(low, p) then return true end end return false end function clofix_main(request) -- WAF bot score check if request.bot_score > 80 then return { action="block", status=403, rule_name="BOT_001", attack_type="Bot", log=true } end if request.is_headless then return { action="challenge", status=429, rule_name="BOT_002", attack_type="Headless Browser", log=true } end -- UA pattern check local ua = request.user_agent or "" if is_bad_ua(ua) then clofix.log_attack("BOT_003", "Bad User-Agent", ua:sub(1,80)) return { action="block", status=403, rule_name="BOT_003", attack_type="Bot UA", log=true } end return { action="allow" } end
Path-Based IP Whitelist
local PROTECTED = { ["/admin"]=true, ["/wp-admin"]=true } local ALLOWED = { ["192.168.1.100"]=true, ["10.0.0.50"]=true } function clofix_main(request) for path in pairs(PROTECTED) do if utils.starts_with(request.path, path) then if not ALLOWED[request.ip] then clofix.log_attack("PATH_001", "Unauthorised Path Access") return { action="block", status=403, rule_name="PATH_001", log=true } end break end end return { action="allow" } end
SQL Injection Detection
local SQLI = { "union%s+select", "select.+from", "insert%s+into", "delete%s+from", "drop%s+table", "or%s+1%s*=%s*1", "sleep%s*%(", "benchmark%s*%(", "information_schema", "xp_cmdshell", "waitfor%s+delay", } local function has_sqli(s) if not s then return false end local norm = clofix.normalize_payload(s) -- multi-pass decode + lowercase for _, p in ipairs(SQLI) do if string.find(norm, p) then return true end end return false end function clofix_main(request) local surfaces = { request.raw_query, request.body, } for k, v in pairs(request.query) do surfaces[#surfaces+1] = v end for k, v in pairs(request.form_data) do surfaces[#surfaces+1] = v end for _, s in ipairs(surfaces) do if has_sqli(s) then clofix.log_attack("SQLI_001", "SQL Injection", (s or ""):sub(1,100)) return { action="block", status=403, rule_name="SQLI_001", attack_type="SQL Injection", log=true } end end return { action="allow" } end
Multi-Rule Scoring Engine
function clofix_main(request) local score = 0 local top_rule, top_type = "", "" local function add(pts, rule, atype) score = score + pts if pts > 0 and top_rule == "" then top_rule, top_type = rule, atype end end -- Threat intel if request.is_tor then add(50, "TI_001", "Tor") end if clofix.ip_reputation()=="malicious" then add(60,"TI_002","Bad Reputation") end -- Bot signals if request.bot_score > 80 then add(50, "BOT_001", "Bot") end if request.bot_score > 50 then add(25, "BOT_002", "Suspect Bot") end if request.is_headless then add(30, "BOT_003", "Headless") end -- UA checks local ua = utils.to_lower(request.user_agent or "") if ua == "" then add(20, "UA_001", "No User-Agent") end if utils.contains(ua,"sqlmap") then add(80, "UA_002", "sqlmap UA") end if utils.contains(ua,"nikto") then add(80, "UA_003", "nikto UA") end -- JA3 known-bad fingerprint local known_bad_ja3 = { ["abc123fingerprint"]=true } if known_bad_ja3[clofix.get_ja3()] then add(70, "FP_001", "Bad JA3") end if score > 0 then clofix.log_attack(top_rule, top_type) end return { score = score, rule_name = top_rule, attack_type = top_type, log = score >= 20, } end
17 Attack-Specific Scripts
XSS Detection
local XSS = { "<%s*script", "javascript%s*:", "on%a+%s*=", "<%s*iframe", "<%s*svg", "expression%s*%(", "vbscript%s*:", "document%.cookie", "eval%s*%(", "alert%s*%(", "window%.location", } local function has_xss(s) if not s or s == "" then return false end local norm = clofix.normalize_payload(s) for _, p in ipairs(XSS) do if string.find(norm, p) then return true end end return false end function clofix_main(request) local surfaces = { request.raw_query, request.body } for _, v in pairs(request.query) do surfaces[#surfaces+1] = v end for _, v in pairs(request.form_data) do surfaces[#surfaces+1] = v end for _, s in ipairs(surfaces) do if has_xss(s) then clofix.log_attack("XSS_001", "Cross-Site Scripting", (sor""):sub(1,80)) return { action="block", status=403, rule_name="XSS_001", attack_type="XSS", log=true } end end return { action="allow" } end
Path Traversal / LFI / RFI
local TRAVERSAL = { "%.%./", "%.\\.%.", "%%2e%%2e", "%%252e%%252e", "%z", "%%00", "/etc/passwd", "/etc/shadow", "/proc/self", "/windows/system32", "php://", "file://", "ftp://", "boot%.ini", } local function has_traversal(s) if not s then return false end local norm = clofix.normalize_payload(s) for _, p in ipairs(TRAVERSAL) do if string.find(norm, p) then return true end end return false end function clofix_main(request) local checks = { request.uri, request.raw_query } for _, v in pairs(request.query) do checks[#checks+1] = v end for _, v in pairs(request.form_data) do checks[#checks+1] = v end for _, s in ipairs(checks) do if has_traversal(s) then clofix.log_attack("PT_001", "Path Traversal", (sor""):sub(1,80)) return { action="block", status=403, rule_name="PT_001", attack_type="Path Traversal", log=true } end end return { action="allow" } end
Command Injection
local CMDI = { "|%s*%a", ";%s*%a", "&&%s*%a", "`[^`]+`", "%$%(.-%)?", "/bin/sh", "/bin/bash", "cmd%.exe", "wget%s+http", "curl%s+http", "nc%s+-", "base64%s+%-d", "python%s+-c", "perl%s+-e", "php%s+-r", } local function has_cmdi(s) if not s then return false end local norm = clofix.normalize_payload(s) for _, p in ipairs(CMDI) do if string.find(norm, p) then return true end end return false end function clofix_main(request) local surfaces = { request.raw_query, request.body } for _, v in pairs(request.form_data) do surfaces[#surfaces+1] = v end for _, s in ipairs(surfaces) do if has_cmdi(s) then clofix.log_attack("CMDI_001", "Command Injection", (sor""):sub(1,80)) return { action="block", status=403, rule_name="CMDI_001", attack_type="Command Injection", log=true } end end return { action="allow" } end
Brute Force Login Protection
lua_shared_dict ddos_attack 10m;local CONFIG = { LOGIN_PATHS = { "/login", "/wp-login.php", "/api/auth", "/signin" }, MAX_ATTEMPTS = 10, WINDOW = 300, BAN_TIME = 1800, } function clofix_main(request) local ip = request.ip local dict = clofix.shared.ddos_attack local now = os.time() local ban = dict:get("bf_ban:" .. ip) or 0 if ban > now then return { action="block", status=403, rule_name="BF_001", attack_type="Brute Force", log=false } end if request.method ~= "POST" then return { action="allow" } end local is_login = false for _, p in ipairs(CONFIG.LOGIN_PATHS) do if utils.starts_with(request.path, p) then is_login = true; break end end if not is_login then return { action="allow" } end local key = "bf_cnt:" .. ip local cnt = dict:incr(key, 1) dict:set(key, cnt, CONFIG.WINDOW) if cnt >= CONFIG.MAX_ATTEMPTS then dict:set("bf_ban:" .. ip, now + CONFIG.BAN_TIME, CONFIG.BAN_TIME) dict:delete(key) clofix.log_attack("BF_001", "Brute Force") return { action="rate_limit", status=429, rule_name="BF_001", attack_type="Brute Force", log=true } end return { action="allow" } end
DDoS Shield
lua_shared_dict ddos_attack 10m;local CONFIG = { BURST=30, SUSTAINED=200, BAN1=60, BAN2=600, BAN3=86400 } local function escalate(dict, ip, now) local ok = "ddos_off:" .. ip local n = (dict:get(ok) or 0) + 1 dict:set(ok, n, 86400) local dur = n==1 and CONFIG.BAN1 or n==2 and CONFIG.BAN2 or CONFIG.BAN3 dict:set("ddos_ban:"..ip, now+dur, dur) clofix.log("warn", string.format("[DDOS] %s offense #%d banned %ds", ip, n, dur)) end function clofix_main(request) local ip = request.ip local dict = clofix.shared.ddos_attack local now = os.time() if (dict:get("ddos_ban:"..ip) or 0) > now then return { action="rate_limit", status=429 } end local sk = "ddos_s:"..ip..":"..now local sc = dict:incr(sk, 1); dict:set(sk, sc, 2) if sc > CONFIG.BURST then escalate(dict, ip, now) return { action="rate_limit", status=429, rule_name="DDOS_001", attack_type="DDoS Burst", log=true } end local mk = "ddos_m:"..ip..":"..os.date("%Y%m%d%H%M") local mc = dict:incr(mk, 1); dict:set(mk, mc, 70) if mc > CONFIG.SUSTAINED then escalate(dict, ip, now) return { action="rate_limit", status=429, rule_name="DDOS_002", attack_type="DDoS Sustained", log=true } end return { action="allow" } end
HTTP Method Filter
local GLOBAL = { GET=true,POST=true,PUT=true,DELETE=true,PATCH=true,OPTIONS=true,HEAD=true } local READ = { GET=true,HEAD=true,OPTIONS=true } local API_OK = { GET=true,POST=true,PUT=true,DELETE=true,PATCH=true } local RO_PATHS = { "/blog","/docs","/static","/assets" } local API_PATHS = { "/api/","/v1/","/v2/" } function clofix_main(request) local m = string.upper(request.method or "") if not GLOBAL[m] then return { action="block", status=405, rule_name="MTH_001", attack_type="Bad Method", log=true } end for _, p in ipairs(RO_PATHS) do if utils.starts_with(request.path, p) and not READ[m] then return { action="block", status=405, rule_name="MTH_002", log=true } end end for _, p in ipairs(API_PATHS) do if utils.starts_with(request.path, p) and not API_OK[m] then return { action="block", status=405, rule_name="MTH_003", log=true } end end return { action="allow" } end
Request Size Limiter
local CONFIG = { MAX_BODY = 1048576, -- 1 MB MAX_URI = 2048, MAX_HEADERS = 50, PATH_LIMITS = { ["/api/upload"]=10485760, ["/media/ingest"]=52428800 }, } function clofix_main(request) if #(request.urior"") > CONFIG.MAX_URI then return { action="block", status=414, rule_name="SIZE_001", log=true } end local cl = request.content_length or 0 local limit = CONFIG.PATH_LIMITS[request.path] or CONFIG.MAX_BODY if cl > limit then return { action="block", status=413, rule_name="SIZE_002", log=true } end local hc = 0 for _ in pairs(request.headers) do hc=hc+1 end if hc > CONFIG.MAX_HEADERS then return { action="block", status=431, rule_name="SIZE_003", log=true } end return { action="allow" } end
Tor / VPN / Cloud Proxy Blocker
local CONFIG = { BLOCK_TOR=true, BLOCK_VPN=false, BLOCK_CLOUD=false, BLOCK_HEADLESS=true, EXEMPT_PATHS={ "/api/public","/health","/status" } } function clofix_main(request) for _, p in ipairs(CONFIG.EXEMPT_PATHS) do if utils.starts_with(request.path, p) then return { action="allow" } end end if CONFIG.BLOCK_TOR and clofix.is_tor() then return { action="block",status=403,rule_name="ANON_001",attack_type="Tor",log=true } end if CONFIG.BLOCK_VPN and clofix.is_vpn() then return { action="block",status=403,rule_name="ANON_002",attack_type="VPN",log=true } end if CONFIG.BLOCK_CLOUD and request.is_cloud_provider then return { action="block",status=403,rule_name="ANON_003",attack_type="Cloud IP",log=true } end if CONFIG.BLOCK_HEADLESS and request.is_headless then return { action="block",status=403,rule_name="ANON_004",attack_type="Headless",log=true } end return { action="allow" } end
WordPress Shield
lua_shared_dict ddos_attack 10m;local CONFIG = { BLOCK_XMLRPC=true, BLOCK_AUTHOR_ENUM=true, BLOCK_PHP_UPLOADS=true, LOGIN_LIMIT=5, BAN_TIME=900, BLOCKED_PATHS={ "/wp-config.php","/wp-content/debug.log","/.git","/.env","/readme.html" }, } function clofix_main(request) local path = utils.to_lower(request.path or "/") local ip = request.ip local dict = clofix.shared.ddos_attack local now = os.time() if CONFIG.BLOCK_XMLRPC and path == "/xmlrpc.php" then return { action="block",status=403,rule_name="WP_001",attack_type="XML-RPC Abuse",log=true } end if CONFIG.BLOCK_AUTHOR_ENUM and request.query["author"] then return { action="block",status=403,rule_name="WP_002",attack_type="Author Enum",log=true } end for _, p in ipairs(CONFIG.BLOCKED_PATHS) do if utils.starts_with(path, p) then return { action="block",status=403,rule_name="WP_003",attack_type="Sensitive File",log=true } end end if CONFIG.BLOCK_PHP_UPLOADS and utils.contains(path, "/wp-content/uploads") and utils.ends_with(path, ".php") then return { action="block",status=403,rule_name="WP_004",attack_type="Webshell Upload",log=true } end if path == "/wp-login.php" and request.method == "POST" then local bk = "wp_ban:"..ip if (dict:get(bk)or0) > now then return { action="rate_limit",status=429 } end local ck = "wp_cnt:"..ip local c = dict:incr(ck, 1); dict:set(ck, c, 60) if c > CONFIG.LOGIN_LIMIT then dict:set(bk, now+CONFIG.BAN_TIME, CONFIG.BAN_TIME) clofix.log_attack("WP_005", "WP Brute Force") return { action="rate_limit",status=429,rule_name="WP_005",log=true } end end return { action="allow" } end
Pentest Scanner Detection
lua_shared_dict ddos_attack 10m;local SCANNER_UA = { "nmap","nikto","sqlmap","burp","nuclei","zgrab", "masscan","zap","acunetix","nessus","openvas", "gobuster","dirbuster","wfuzz","hydra", "python-requests","go-http-client","libwww-perl" } local SCANNER_PATHS = { "/.git/","/.env","/.htaccess","/etc/passwd", "/phpmyadmin","/manager/html","/solr/","/jenkins", "/actuator","/cgi-bin/","/backup","/dump.sql", "/.DS_Store","/setup.php","/install.php" } local BAN_TIME = 3600 function clofix_main(request) local ip = request.ip local ua = utils.to_lower(request.user_agent or "") local path = utils.to_lower(request.path or "/") local dict = clofix.shared.ddos_attack local now = os.time() if (dict:get("scan_ban:"..ip)or0) > now then return { action="block",status=403 } end local reason = nil for _, s in ipairs(SCANNER_UA) do if utils.contains(ua, s) then reason = "ua:"..s; break end end if not reason then for _, p in ipairs(SCANNER_PATHS) do if utils.contains(path, p) then reason = "path:"..p; break end end end if reason then dict:set("scan_ban:"..ip, now+BAN_TIME, BAN_TIME) clofix.log_attack("SCAN_001", "Scanner", reason) return { action="block",status=403,rule_name="SCAN_001",attack_type="Scanner",log=true } end return { action="allow" } end
18 More Useful Scripts
Password Protect Any URL
local CONFIG = { SECRET="change-me-strong-secret", COOKIE_NAME="clofix_access", COOKIE_TTL=86400, PROTECTED_PATHS={ "/staging","/preview","/beta","/internal" }, TRUSTED_IPS={ ["192.168.1.0"]=true }, } function clofix_main(request) local path = request.path or "/" local protected = false for _, p in ipairs(CONFIG.PROTECTED_PATHS) do if utils.starts_with(path, p) then protected = true; break end end if not protected or CONFIG.TRUSTED_IPS[request.ip] then return { action="allow" } end if request.cookies[CONFIG.COOKIE_NAME] == CONFIG.SECRET then return { action="allow" } end local token = request.query["token"] local auth = request.headers["authorization"] or "" if not token then token = string.match(auth, "^[Bb]earer%s+(.+)$") end if token == CONFIG.SECRET then clofix.header["Set-Cookie"] = string.format( "%s=%s; Path=/; Max-Age=%d; HttpOnly; SameSite=Strict", CONFIG.COOKIE_NAME, CONFIG.SECRET, CONFIG.COOKIE_TTL) return { action="allow" } end clofix.header["WWW-Authenticate"] = 'Bearer realm="Protected Area"' return { action="block", status=401, rule_name="AUTH_001", log=true } end
API Key Authentication
local API_KEYS = { ["key-client-alpha-1234"] = { name="client-alpha", rate=100 }, ["key-client-beta-5678"] = { name="client-beta", rate=500 }, ["key-internal-9999"] = { name="internal", rate=0 }, } local PROTECTED = { "/api/","/v1/","/v2/","/graphql" } function clofix_main(request) local needs = false for _, p in ipairs(PROTECTED) do if utils.starts_with(request.path, p) then needs=true;break end end if not needs then return { action="allow" } end local key = request.headers["x-api-key"] or request.headers["x-api-token"] or request.query["api_key"] if not key or key == "" then clofix.header["WWW-Authenticate"] = 'APIKey realm="API"' return { action="block",status=401,rule_name="APIKEY_001" } end local client = API_KEYS[key] if not client then clofix.log_attack("APIKEY_002","Invalid API Key") return { action="block",status=403,rule_name="APIKEY_002",log=true } end if client.rate > 0 then local rk = "ak:"..key..":"..os.date("%Y%m%d%H%M") local rl = clofix.shared.rate_limit local cnt = rl:incr(rk,1); rl:set(rk,cnt,70) if cnt > client.rate then clofix.header["Retry-After"] = "60" return { action="rate_limit",status=429,rule_name="APIKEY_003",log=true } end end return { action="allow" } end
Hotlink Protection
local ALLOWED_DOMAINS = { "example.com","www.example.com","cdn.example.com" } local ASSET_EXT = { "%.jpg","%.jpeg","%.png","%.gif","%.webp","%.svg","%.mp4","%.pdf","%.zip" } local ALLOW_NO_REF = true local function is_asset(path) local low = utils.to_lower(path) for _, e in ipairs(ASSET_EXT) do if string.find(low, e.."$") then return true end end return false end local function ok_ref(ref) if not ref or ref == "" then return ALLOW_NO_REF end local low = utils.to_lower(ref) for _, d in ipairs(ALLOWED_DOMAINS) do if utils.contains(low, d) then return true end end return false end function clofix_main(request) if not is_asset(request.path) then return { action="allow" } end local ref = request.referer or request.headers["referer"] or "" if not ok_ref(ref) then return { action="block",status=403,rule_name="HOTLINK_001",attack_type="Hotlinking",log=true } end return { action="allow" } end
Dangerous File Type Filter
local BLOCKED_EXT = { "%.php","%.php3","%.php5","%.phtml","%.asp","%.aspx","%.jsp", "%.cgi","%.pl","%.py","%.sh","%.bash","%.exe","%.bat","%.ps1", "%.jar","%.war","%.htaccess","%.env", } local BLOCKED_CT = { "application/x-php","application/x-executable", "text/x-php","application/x-msdownload", } local UPLOAD_PATHS = { "/upload","/uploads","/media","/wp-content/uploads" } function clofix_main(request) local low = utils.to_lower(request.path or "/") for _, e in ipairs(BLOCKED_EXT) do if string.find(low, e) then return { action="block",status=403,rule_name="FT_001",attack_type="Bad File Extension",log=true } end end if request.method == "POST" then local ct = utils.to_lower(request.content_type or "") for _, t in ipairs(BLOCKED_CT) do if utils.contains(ct, t) then return { action="block",status=415,rule_name="FT_002",attack_type="Bad Content-Type",log=true } end end end return { action="allow" } end
CSRF Token Validation
local CONFIG = { ALLOWED_ORIGINS = { "https://example.com","https://www.example.com" }, CSRF_HEADER = "x-csrf-token", CSRF_FORM = "_csrf", EXEMPT_PREFIXES = { "/api/webhook","/api/public" }, PROTECTED_METHODS= { POST=true,PUT=true,DELETE=true,PATCH=true }, } function clofix_main(request) local m = string.upper(request.method or "GET") if not CONFIG.PROTECTED_METHODS[m] then return { action="allow" } end for _, p in ipairs(CONFIG.EXEMPT_PREFIXES) do if utils.starts_with(request.path, p) then return { action="allow" } end end local origin = request.headers["origin"] or "" if origin ~= "" then for _, o in ipairs(CONFIG.ALLOWED_ORIGINS) do if origin == o then return { action="allow" } end end return { action="block",status=403,rule_name="CSRF_001",attack_type="CSRF",log=true } end local token = request.headers[CONFIG.CSRF_HEADER] or request.form_data[CONFIG.CSRF_FORM] if not token or #token < 16 then return { action="block",status=403,rule_name="CSRF_002",attack_type="CSRF",log=true } end return { action="allow" } end
Honeypot Bot Trap
lua_shared_dict ddos_attack 10m;local CONFIG = { BAN_TIME=86400, TRAP_PATHS={ "/trap-link","/do-not-visit","/honeypot","/hidden-login", "/admin/config","/wp-json/wp/v2/users","/autodiscover/autodiscover.xml" }, TRUSTED_AGENTS={ "googlebot","bingbot","slurp","duckduckbot" }, } function clofix_main(request) local ip = request.ip local path = utils.to_lower(request.path or "/") local ua = utils.to_lower(request.user_agent or "") local dict = clofix.shared.ddos_attack local now = os.time() if (dict:get("hp_ban:"..ip)or0) > now then return { action="block",status=403 } end for _, trap in ipairs(CONFIG.TRAP_PATHS) do if path == trap then local trusted = false for _, a in ipairs(CONFIG.TRUSTED_AGENTS) do if utils.contains(ua, a) then trusted=true;break end end if not trusted then dict:set("hp_ban:"..ip, now+CONFIG.BAN_TIME, CONFIG.BAN_TIME) clofix.log_attack("HP_001","Honeypot", trap) return { action="block",status=404,rule_name="HP_001",attack_type="Bot",log=true } end return { action="block",status=404 } end end return { action="allow" } end
Maintenance Mode
local CONFIG = { ENABLED=false, ALLOWED_IPS={ ["203.0.113.10"]=true }, ALLOWED_CIDR24={ "192.168.1.","10.0.0." }, ALWAYS_ON={ "/health","/ping","/status","/favicon.ico" }, } function clofix_main(request) if not CONFIG.ENABLED then return { action="allow" } end for _, p in ipairs(CONFIG.ALWAYS_ON) do if request.path == p then return { action="allow" } end end local ip = request.ip if CONFIG.ALLOWED_IPS[ip] then return { action="allow" } end for _, pfx in ipairs(CONFIG.ALLOWED_CIDR24) do if utils.starts_with(ip, pfx) then return { action="allow" } end end clofix.header["Retry-After"] = "3600" clofix.header["Cache-Control"] = "no-store" return { action="block",status=503,rule_name="MAINT_001",log=false } end
Smart Redirect Engine
local EXACT = { ["/home"] = { to="/", code=301 }, ["/about-us"] = { to="/about", code=301 }, ["/old-pricing"] = { to="/pricing", code=301 }, } local PREFIX = { { from="/old-blog/", to="/blog/", code=301 }, { from="/legacy/api/", to="/api/v2/", code=302 }, } local PATTERNS = { { from="^/products/(%d+)$", to="/shop/item/%1", code=301 }, { from="^(/[^?]+)/$", to="%1", code=301 }, } function clofix_main(request) local path = request.path or "/" local ex = EXACT[path] if ex then clofix.header["Location"] = ex.to return { action="redirect",status=ex.code,redirect_to=ex.to } end for _, r in ipairs(PREFIX) do if utils.starts_with(path, r.from) then local dest = r.to .. path:sub(#r.from+1) clofix.header["Location"] = dest return { action="redirect",status=r.code,redirect_to=dest } end end for _, r in ipairs(PATTERNS) do local dest = string.gsub(path, r.from, r.to) if dest ~= path then clofix.header["Location"] = dest return { action="redirect",status=r.code,redirect_to=dest } end end return { action="allow" } end
19 Testing
Diagnostic Script
function clofix_main(request) clofix.log("info", "=== DEBUG ===") clofix.log("info", "IP: " .. tostring(request.ip)) clofix.log("info", "Method: " .. tostring(request.method)) clofix.log("info", "Path: " .. tostring(request.path)) clofix.log("info", "UA: " .. tostring(request.user_agent)) clofix.log("info", "Country: " .. tostring(request.country)) clofix.log("info", "JA3: " .. tostring(request.ja3)) clofix.log("info", "JA4: " .. tostring(request.ja4)) clofix.log("info", "Bot score: " .. tostring(request.bot_score)) clofix.log("info", "Reputation: " .. tostring(request.ip_reputation)) clofix.log("info", "Anomaly: " .. tostring(request.anomaly_score)) clofix.log("info", "Is Tor: " .. tostring(request.is_tor)) return { action="allow" } end
Shell Commands
# Watch Lua log output sudo tail -f /var/log/clofix/waf.log | grep -i '\[LUA\]' # Normal request curl https://your-domain.com/ # Simulate bad UA (scanner) curl -A 'sqlmap/1.7' https://your-domain.com/ # SQL injection in query curl 'https://your-domain.com/?id=1%20UNION%20SELECT%20*%20FROM%20users' # Rate limit stress test for i in $(seq 1 200); do curl -s -o /dev/null https://your-domain.com/ & done; wait # Validate syntax luac -p ./lua_script/your-domain.com/script.lua
20 Troubleshooting
| Error / Symptom | Cause | Fix |
|---|---|---|
| 'clofix_main' not found | Function missing or misnamed | Rename your function to exactly clofix_main |
| attempt to call non-function | Entry point not defined | Add: function clofix_main(request) … end |
| attempt to index global 'clofix' | Shared dict not declared in config | Add lua_shared_dict <n> <size>m; |
| nil value on request field | Field not populated | Guard: local x = field or "default" |
| Lua execution timed out (503) | Script exceeds 50 ms | Move heavy lookups to shared dict; avoid nested loops |
| stack overflow | Infinite recursion | Check for recursive function calls |
| syntax error on load | Lua parse error | Run: luac -p /path/to/script.lua |
| changes not taking effect | File not saved to disk | Scripts reload per-request; verify file was saved |
21 Best Practices
- One script per concern — keep
rate_limit.lua,bot_detection.lua,sqli.luaseparate for clarity and easy toggling - Always use clofix.normalize_payload() before pattern matching — catches multi-encoded bypass attempts automatically
- Guard every field — use
local x = field or defaultbefore using any request value - Return early — check block-list lookups first before heavy pattern scanning
- Use clofix.log_attack() instead of manual log strings — it auto-includes IP, country, UA, method, and URI
- Set TTLs on all shared-dict keys — prevents unbounded memory growth
- Use the scoring engine for multi-signal rules — return
scoreinstead of hard-coded action when combining multiple weak signals - Validate with luac -p before deploying — catches syntax errors before they reach production
- CONFIG tables at the top — put all tuneable values in a local CONFIG table so operators can adjust without touching logic
- Start minimal — deploy a working allow-all script first, then layer rules one at a time
- Remove debug logs in production — delete
clofix.log("info", …)calls to avoid log flooding - Numeric prefixes on filenames — use
01_fast.lua,02_heavy.luato control execution order