Lua SDK
v2.0 — clofix_main API

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.

clofix_main entry point Structured action returns Threat scoring engine clofix.* helper API utils.* string tools Rate limiting API JA3 / JA4 fingerprint GeoIP & threat fields Shared dictionaries Execution limits Bot score IP reputation

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.

⚙️ CloFix WAF Lua Architecture
HTTP Request WAF Engine
(IP, Geo, TLS, Threat Intel)
Lua VM
(Isolated per script)
Shared Dicts
(State / Rate Limit)
Action / Backend
Breaking Change — v2.0 The entry point is now clofix_main(request) — not process_request. Scripts must return a single action table, not a boolean pair. See Section 04 & 05 for the new structure.

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

A01:2021-Broken Access Control Path/IP whitelisting, admin area protection, API key auth
A02:2021-Cryptographic Failures Enforce HTTPS, detect missing security headers (HSTS)
A03:2021-Injection SQLi, NoSQLi, CMDi, LDAP, XPath via pattern matching
A04:2021-Insecure Design Rate limiting, brute-force protection, bot mitigation
A05:2021-Security Misconfiguration Block debug paths, .git, .env, directory listing attempts
A06:2021-Vulnerable Components Detect scanner UA (nikto, nuclei) and known exploit paths
A07:2021-Identification/Auth Failures Credential stuffing detection, MFA bypass monitoring
A08:2021-Software/Data Integrity Prevent malicious file uploads, block webshell extensions
A09:2021-Security Logging/Monitoring clofix.log_attack() with full request context
A10:2021-SSRF Block internal IPs/hostnames in URL parameters

🔒 Comprehensive Protection Matrix — What You Can Build with Lua

💉 INJECTION & PAYLOAD ATTACKS
💾 SQL Injection (UNION, OR, SLEEP)
📊 NoSQL Injection (MongoDB operators)
🐚 Command Injection (; | && ` $() )
📜 XSS (Reflected, Stored, DOM-based)
📁 Path Traversal (../, ..\, URL encoded)
🌐 SSRF (Internal IPs, localhost, metadata)
📋 LDAP Injection
🔤 XPath Injection
📧 SMTP/Header Injection
🧩 Template Injection (SSTI)
OGNL/EL Injection
🔌 HTTP Parameter Pollution
🤖 BOT & AUTOMATED THREATS
🕷️ Web Scraping / Crawlers
🔑 Credential Stuffing
🔨 Brute Force (Login, API, OTP)
📱 Headless Browsers (Puppeteer, Playwright)
🤖 AI Bots / GPT Crawlers
🔄 Content Scraping / Price Harvesting
📊 Form Spam / Comment Spam
🎣 Phishing Kit Detection
⏱️ RATE LIMITING & DOS PROTECTION
🌍 Per-IP Rate Limiting
🔑 Per-API Key / Token Quotas
📁 Per-Endpoint / Path Limits
🌐 Per-Session / User ID Limits
Burst Protection (DDoS mitigation)
🛡️ Layer 7 DDoS Shield
Time-Based Access Control
📊 Concurrent Connection Limits
🌍 GEOLOCATION & IP REPUTATION
🚫 Country Blocking (sanctions, high-risk)
🧅 Tor Exit Node Detection
🔒 VPN / Proxy / Datacenter Detection
☁️ Cloud Provider IPs (AWS, GCP, Azure)
⚠️ Malicious IP Reputation (blacklists)
🏢 ASN / ISP-based filtering
📍 City/Region specific rules
📡 Anonymizer / Open Proxy Detection
🔐 ACCESS CONTROL & AUTHENTICATION
🔑 API Key Validation & Rotation
🔐 JWT Validation (expiry, signature, claims)
🔒 Basic Auth / Bearer Token enforcement
🛡️ IP Whitelisting / Blacklisting
🚪 Admin Area Protection
🔓 Password Protect Staging/Dev URLs
🍪 Session Hijacking Prevention
🔄 CSRF Token Validation
📱 MFA Enforcement Check
🕒 Session Timeout / Inactivity Logout
📁 FILE & UPLOAD SECURITY
🚫 Dangerous File Extensions (.php, .exe, .sh)
🖼️ Malicious Image / Polyglot Detection
📏 File Size Limits (per endpoint)
🔍 MIME Type Validation
🧪 Upload-to-Webshell Prevention
🔒 Executable Upload Blocking
📦 Archive Bomb / Zip Bomb Detection
🖨️ Sensitive File Access (.env, .git, config)
📡 PROTOCOL & HEADER SECURITY
🔒 HTTPS Enforcement (redirect HTTP)
📋 HTTP Method Restriction (GET/POST only)
🛡️ Security Headers (HSTS, CSP, X-Frame-Options)
🔍 User-Agent Filtering / Blocking
🌐 Referer / Origin Validation
📏 Header Size / Count Limits
🚫 Block Suspicious Headers (X-Forwarded-Host abuse)
🔄 Host Header Injection Protection
🔐 TLS Fingerprint (JA3/JA4) Validation
🌍 CORS Policy Enforcement
💼 BUSINESS LOGIC & ABUSE PREVENTION
🛒 Coupon / Promo Code Brute Force
🎫 Ticket / Inventory Hoarding Prevention
📧 Email Verification / Rate Limit Signups
🔁 Account Takeover Detection
💰 Payment Fraud / Carding Attempts
📊 Vote / Poll Manipulation
📈 Click Fraud / Ad Fraud Detection
🔄 API Abuse / Webhook Bombing
🚫 Competitor Data Scraping
📦 CMS & PLATFORM SPECIFIC
🔐 WordPress: XML-RPC Block
👤 WordPress: Author Enumeration
📁 WordPress: PHP in Uploads Block
🛒 WooCommerce: Bot Checkout Prevention
📋 Joomla / Drupal exploit paths
⚙️ phpMyAdmin / Adminer Exposure
🔧 Laravel Debug Mode Block
🐍 Django / Flask debug endpoints
⚡ ADVANCED & CUSTOM LOGIC
🔄 Smart Redirects (A/B testing, geo redirect)
🔌 Canary / Honeypot Traps
Maintenance Mode / Scheduled Access
📊 A/B Testing Traffic Splitting
🔍 Request Mirroring / Debugging
📝 Custom Audit Logging
🛡️ Dynamic IP Reputation Scoring
🔮 Machine Learning Model Integration
🔐 Zero-Trust Security Policies
🌐 Multi-Tenant Isolation Rules

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.

pipeline
  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

flow
  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

lua — request fields
-- ── 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

lua — return table
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.*

lua — clofix.* cheatsheet
-- ── 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  (&amp; → &)
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.*

lua — utils.* cheatsheet
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

FilePurposeShared Dict needed
example_security.luaMulti-rule scoring engine covering SQLi, XSS, CMDi, SSRF, rate limit, bad UA, Tor, reputationddos_attack
wordpress_shield.luaXML-RPC block, author enum, PHP upload guard, wp-login brute forceddos_attack
api_protection.luaAPI key auth + per-key quota + endpoint rate limitingrate_limit

04 Directory Structure

CloFix creates the domain directory automatically when Lua is enabled. You can also create it manually:

filesystem
./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
Execution Order Files execute in filesystem (alphabetical) order. Use numeric prefixes like 01_, 02_ to control priority. Cheaper checks (block-list lookup) should run before heavy pattern matching.

05 Domain Configuration

Enable Lua and declare shared dictionaries inside your domain config or via the CloFix Config Manager dashboard:

nginx config
# ./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

lua
function clofix_main(request)
    -- Your logic here
    return { action = "allow", status = 200 }
end

Full Template

lua
-- ① 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

FieldTypeRequiredDescription
actionstringOne of the action types below
statusnumberHTTP status code. Auto-filled if omitted (403 for block, 429 for rate_limit, etc.)
messagestringHuman-readable reason. Logged and included in alerts.
rule_namestringRule identifier e.g. SQLI_001. Appears in WAF logs.
attack_typestringAttack category e.g. SQL Injection.
logbooleanForce-emit a log entry. Automatically true for blocking actions.
redirect_tostringTarget URL when action is redirect.
scorenumberUsed by the scoring engine. If action is omitted, score decides the action.

Supported Action Types

allow
Pass request to backend (status 200)
block
Reject immediately (status 403)
challenge
Serve a CAPTCHA / JS challenge (status 429)
rate_limit
Rate limit response (status 429)
log_only
Allow but emit a log entry
redirect
HTTP redirect using redirect_to field
captcha
Require CAPTCHA verification (status 429)

Examples

lua — action 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" }
Legacy Compatibility Scripts that return true, 200 or false, 403 (old boolean pair) still work. The WAF engine detects the boolean return and converts it automatically. New scripts should use the table format.

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.

< 20
allow
20 – 49
log_only
50 – 79
challenge
≥ 80
block
lua — scoring pattern
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

FieldTypeDescriptionExample
ipstringClient IP address (alias of remote_ip)"203.0.113.5"
methodstringHTTP method"GET", "POST"
schemestringProtocol scheme"https"
hoststringHost header value"example.com"
hostnamestringHostname without port"example.com"
portnumberClient source port54321
uristringFull URI with query string"/page?id=1"
pathstringURI path only"/page"
raw_querystringRaw query string (unparsed)"id=1&q=test"
raw_urlstringFull raw URL"https://example.com/page?id=1"
protocolstringHTTP version"HTTP/1.1"
remote_ipstringClient IP address"203.0.113.5"
remote_addrstringIP:port combined"203.0.113.5:54321"
timestampnumberUnix timestamp of request1709123456
request_idstringUnique request ID"abc123…"

Body Fields

FieldTypeDescription
bodystringRequest body as UTF-8 string
raw_bodystringRaw body bytes (same as body, binary-safe)
json_bodystringJSON-encoded body if Content-Type is application/json
form_datatableParsed POST form fields

Table Fields — headers, cookies, query

lua
-- 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

FieldTypeDescription
user_agentstringUser-Agent header
refererstringReferer header
content_typestringContent-Type header
content_lengthnumberContent-Length header value
origin_headerstringOrigin header
x_forwarded_forstringX-Forwarded-For header
is_tlsbooleantrue if HTTPS connection
tls_versionstringTLS version string — "TLS 1.3"
tls_cipherstringTLS cipher suite

GeoIP Fields

FieldTypeDescription
countrystringISO-3166 country code — "US", "BD", "CN"
regionstringRegion / state name
citystringCity name
asnstringAutonomous System Number — "AS12345"
orgstringISP or organisation name

Security & Fingerprint Fields

FieldTypeDescription
ja3stringJA3 TLS fingerprint hash
ja4stringJA4 TLS fingerprint hash
is_torbooleantrue if Tor exit node
is_vpnbooleantrue if VPN / proxy detected
is_cloud_providerbooleantrue if AWS / GCP / Azure IP
is_botbooleantrue if WAF bot detection triggered
is_headlessbooleantrue if headless browser detected
bot_scorenumber0–100 bot confidence score (higher = more likely bot)
anomaly_scorenumberCumulative WAF anomaly score
ip_reputationstring"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

FunctionReturnsDescription
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)stringHTML entity decode (&amp; → &)
clofix.normalize_payload(s)stringMulti-pass decode + lowercase + collapse whitespace

Client & Fingerprint Info

FunctionReturnsDescription
clofix.get_ip()stringCurrent client IP
clofix.get_country()stringISO country code of client
clofix.get_asn()stringASN of client
clofix.get_ja3()stringJA3 fingerprint hash
clofix.get_ja4()stringJA4 fingerprint hash
clofix.get_bot_score()number0–100 bot confidence score

Threat Intelligence

FunctionReturnsDescription
clofix.is_tor()booleantrue if client is a Tor exit node
clofix.is_vpn()booleantrue if client is a VPN / proxy
clofix.is_proxy()booleantrue if proxy headers detected (Via, X-Forwarded-For, Forwarded)
clofix.ip_reputation()string"clean" | "suspicious" | "malicious"

Rate Limiting

FunctionSignatureReturnsDescription
clofix.rate_limit(key, limit, window_secs)count, exceededGeneric rate limiter. key can be any string.
clofix.rate_limit_ip(limit, window_secs)count, exceededPer-IP rate limit. Key is ip:<ip>.
clofix.rate_limit_endpoint(limit, window_secs)count, exceededPer-IP per-path rate limit. Key is ep:<ip>:<path>.

Logging

FunctionDescription
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

FunctionReturnsDescription
clofix.shared_dict(name)dict | nil, errGet a shared dictionary by name (same as clofix.shared)
lua — clofix.* usage examples
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.

FunctionReturnsDescription
utils.url_encode(s)stringURL-encode a string
utils.url_decode(s)string, err?URL-decode a string
utils.base64_encode(s)stringBase64 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)stringBuild a URL query string from a table
utils.contains(s, sub)booleantrue if s contains sub (plain match)
utils.starts_with(s, pfx)booleantrue if s starts with pfx
utils.ends_with(s, sfx)booleantrue if s ends with sfx
utils.to_lower(s)stringLowercase a string
utils.to_upper(s)stringUppercase a string
utils.trim(s)stringTrim leading/trailing whitespace
utils.split(s, sep)tableSplit string by separator into array
utils.log(msg)Log a message at INFO level
utils.now()numberCurrent Unix timestamp
lua — utils.* usage
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

lua
-- 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)

MethodSignatureReturnsDescription
getdict:get(key)value | nilRead value; nil if missing or expired
setdict:set(key, value, ttl?)Store value with optional TTL in seconds
incrdict:incr(key, delta)new numberAtomically increment a numeric counter
deletedict:delete(key)Remove a key immediately
flush_alldict:flush_all()Clear all keys in the dictionary
ttldict:ttl(key)secondsRemaining 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.

lua — clofix rate limit API
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

MethodLevelUse for
clofix.log("info", msg)INFONormal informational events
clofix.log("warn", msg)WARNBlocked requests, suspicious activity
clofix.log("error", msg)ERRORScript errors, unexpected conditions
clofix.log_attack(rule, type, payload?)WARNStructured attack log with full request context auto-included
utils.log(msg)INFOSimple one-arg info log shorthand
lua
-- 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.

LimitDefaultPurpose
Wall-clock timeout50 msScript that takes longer is killed and the request is blocked with 503
Call stack depth200 framesPrevents infinite recursion / stack overflow
Memory32 MBLua allocator limit per script execution
Timeout Behaviour If a script exceeds 50 ms the WAF returns HTTP 503 and logs [LUA] script execution timed out. Keep pattern matching O(n) and avoid nested loops over large datasets inside the hot path.

16 Core Examples

Rate Limiter

rate_limit.lua
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

geo_block.lua
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

bot_detection.lua
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

path_protection.lua
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

sql_injection.lua
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

threat_scoring.lua
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

xss_protection.lua
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

path_traversal.lua
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

cmd_injection.lua
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

Config requiredlua_shared_dict ddos_attack 10m;
brute_force.lua
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

Config requiredlua_shared_dict ddos_attack 10m;
ddos_shield.lua
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

method_filter.lua
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

request_size.lua
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

anonymiser_block.lua
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

Config requiredlua_shared_dict ddos_attack 10m;
wordpress_shield.lua
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

Config requiredlua_shared_dict ddos_attack 10m;
scanner_detect.lua
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

password_protect.lua
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

api_key_auth.lua
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_protect.lua
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

file_type_filter.lua
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

csrf_protect.lua
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

Config requiredlua_shared_dict ddos_attack 10m;
honeypot.lua
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

maintenance_mode.lua
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

redirects.lua
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

debug_request.lua — remove before production
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

shell
# 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 / SymptomCauseFix
'clofix_main' not foundFunction missing or misnamedRename your function to exactly clofix_main
attempt to call non-functionEntry point not definedAdd: function clofix_main(request) … end
attempt to index global 'clofix'Shared dict not declared in configAdd lua_shared_dict <n> <size>m;
nil value on request fieldField not populatedGuard: local x = field or "default"
Lua execution timed out (503)Script exceeds 50 msMove heavy lookups to shared dict; avoid nested loops
stack overflowInfinite recursionCheck for recursive function calls
syntax error on loadLua parse errorRun: luac -p /path/to/script.lua
changes not taking effectFile not saved to diskScripts reload per-request; verify file was saved

21 Best Practices

  1. One script per concern — keep rate_limit.lua, bot_detection.lua, sqli.lua separate for clarity and easy toggling
  2. Always use clofix.normalize_payload() before pattern matching — catches multi-encoded bypass attempts automatically
  3. Guard every field — use local x = field or default before using any request value
  4. Return early — check block-list lookups first before heavy pattern scanning
  5. Use clofix.log_attack() instead of manual log strings — it auto-includes IP, country, UA, method, and URI
  6. Set TTLs on all shared-dict keys — prevents unbounded memory growth
  7. Use the scoring engine for multi-signal rules — return score instead of hard-coded action when combining multiple weak signals
  8. Validate with luac -p before deploying — catches syntax errors before they reach production
  9. CONFIG tables at the top — put all tuneable values in a local CONFIG table so operators can adjust without touching logic
  10. Start minimal — deploy a working allow-all script first, then layer rules one at a time
  11. Remove debug logs in production — delete clofix.log("info", …) calls to avoid log flooding
  12. Numeric prefixes on filenames — use 01_fast.lua, 02_heavy.lua to control execution order
Need Help? For CloFix WAF support, configuration questions, or to report Lua scripting issues, visit the Support section or contact us.