使用OpenResty实现Web应用防护

我们的WAF系统采用模块化设计,主要包含以下几个核心组件:

1. 请求生命周期管理

OpenResty提供了多个执行阶段,我们需要在不同阶段执行不同的安全检测:

-- init_by_lua_block: 初始化阶段,加载配置和规则
local config = require "waf.config"
local rules = require "waf.rules"

-- 初始化共享内存
local limit_req_store = ngx.shared.limit_req_store
local blacklist = ngx.shared.blacklist
local whitelist = ngx.shared.whitelist

-- 加载安全规则
function init_waf()
    -- 加载SQL注入规则
    rules.sql_patterns = {
        "select.*from",
        "union.*select",
        "insert.*into",
        "delete.*from",
        "update.*set",
        "drop.*table",
        "exec.*\\(",
        "execute.*\\(",
        "script.*>",
        "javascript:",
        "vbscript:",
        "onload=",
        "onerror=",
        "onclick="
    }
    
    -- 编译正则表达式以提高性能
    for i, pattern in ipairs(rules.sql_patterns) do
        rules.sql_patterns[i] = ngx.re.compile(pattern, "joi")
    end
end

init_waf()

2. IP访问控制模块

IP访问控制是最基础也是最有效的防护手段。我们实现了动态的IP黑白名单管理:

-- ip_filter.lua
local _M = {}
local blacklist = ngx.shared.blacklist
local whitelist = ngx.shared.whitelist

function _M.check_ip()
    local client_ip = ngx.var.remote_addr
    
    -- 白名单优先
    local is_white = whitelist:get(client_ip)
    if is_white then
        return true
    end
    
    -- 检查黑名单
    local is_black = blacklist:get(client_ip)
    if is_black then
        ngx.log(ngx.ERR, "Blocked IP: ", client_ip)
        ngx.status = 403
        ngx.say('{"code": 403, "msg": "Access Denied"}')
        ngx.exit(403)
        return false
    end
    
    return true
end

-- 动态添加黑名单
function _M.add_blacklist(ip, expire_time)
    expire_time = expire_time or 3600  -- 默认封禁1小时
    local succ, err = blacklist:set(ip, true, expire_time)
    if not succ then
        ngx.log(ngx.ERR, "Failed to add blacklist: ", err)
    end
    return succ
end

return _M

3. 速率限制模块

速率限制是防御DDoS和暴力破解攻击的重要手段。我们实现了基于令牌桶算法的限速器:

-- rate_limiter.lua
local _M = {}
local limit_req_store = ngx.shared.limit_req_store

function _M.is_limited(key, rate, burst)
    local now = ngx.now()
    local token_key = "tokens:" .. key
    local time_key = "time:" .. key
    
    -- 获取上次请求时间和剩余令牌
    local last_time = limit_req_store:get(time_key) or 0
    local tokens = limit_req_store:get(token_key) or burst
    
    -- 计算新增令牌
    local elapsed = math.max(0, now - last_time)
    local new_tokens = math.min(burst, tokens + elapsed * rate)
    
    if new_tokens < 1 then
        -- 令牌不足,限速
        return true
    end
    
    -- 消耗一个令牌
    limit_req_store:set(token_key, new_tokens - 1)
    limit_req_store:set(time_key, now)
    
    return false
end

-- 使用示例
function _M.check_rate_limit()
    local client_ip = ngx.var.remote_addr
    local uri = ngx.var.uri
    
    -- 针对不同接口设置不同限速
    local limits = {
        ["/api/login"] = {rate = 1, burst = 5},      -- 登录接口:1次/秒,突发5次
        ["/api/register"] = {rate = 0.5, burst = 3}, -- 注册接口:0.5次/秒,突发3次
        ["default"] = {rate = 10, burst = 50}        -- 默认:10次/秒,突发50次
    }
    
    local limit = limits[uri] or limits["default"]
    local key = client_ip .. ":" .. uri
    
    if _M.is_limited(key, limit.rate, limit.burst) then
        ngx.status = 429
        ngx.header["Retry-After"] = "60"
        ngx.say('{"code": 429, "msg": "Too Many Requests"}')
        ngx.exit(429)
    end
end

return _M

4. SQL注入检测模块

SQL注入是最常见的Web攻击方式之一。我们通过多层检测机制来防御SQL注入:

-- sql_injection.lua
local _M = {}

-- SQL注入检测规则
local sql_patterns = {
    -- 基础SQL语句
    "select.*from", "union.*select", "insert.*into",
    "delete.*from", "update.*set", "drop.*table",
    
    -- SQL函数和操作符
    "concat.*\\(", "group_concat.*\\(", "union.*all",
    "information_schema", "sysobjects", "syscolumns",
    
    -- 注释符
    "--|#|/\\*|\\*/",
    
    -- 特殊字符组合
    "'.*or.*'='", "\".*or.*\"=\"", "1=1", "1=2",
    
    -- 时间盲注
    "sleep\\s*\\(", "benchmark\\s*\\(", "waitfor\\s+delay"
}

function _M.check_sql_injection()
    -- 检查URL参数
    local args = ngx.req.get_uri_args()
    for key, val in pairs(args) do
        if type(val) == "string" then
            if _M.detect_sql_injection(val) then
                _M.block_request("SQL Injection in URL parameter: " .. key)
                return false
            end
        elseif type(val) == "table" then
            for _, v in ipairs(val) do
                if _M.detect_sql_injection(v) then
                    _M.block_request("SQL Injection in URL parameter: " .. key)
                    return false
                end
            end
        end
    end
    
    -- 检查POST数据
    ngx.req.read_body()
    local post_args = ngx.req.get_post_args()
    if post_args then
        for key, val in pairs(post_args) do
            if type(val) == "string" and _M.detect_sql_injection(val) then
                _M.block_request("SQL Injection in POST parameter: " .. key)
                return false
            end
        end
    end
    
    return true
end

function _M.detect_sql_injection(input)
    if not input then return false end
    
    -- 转换为小写进行检测
    local lower_input = string.lower(input)
    
    -- URL解码
    local decoded = ngx.unescape_uri(lower_input)
    
    for _, pattern in ipairs(sql_patterns) do
        if ngx.re.find(decoded, pattern, "joi") then
            ngx.log(ngx.WARN, "SQL injection detected: ", pattern, " in: ", input)
            return true
        end
    end
    
    return false
end

function _M.block_request(reason)
    ngx.log(ngx.ERR, "Blocked request: ", reason)
    ngx.status = 403
    ngx.say('{"code": 403, "msg": "Potential SQL Injection Detected"}')
    ngx.exit(403)
end

return _M

5. XSS攻击防护模块

跨站脚本攻击(XSS)是另一个常见威胁。我们的XSS防护模块采用多重策略:

-- xss_filter.lua
local _M = {}

local xss_patterns = {
    -- JavaScript事件处理器
    "on(load|error|click|mouse|key|submit|focus|blur)\\s*=",
    
    -- Script标签
    "<script[^>]*>.*</script>",
    "<script[^>]*/>",
    
    -- JavaScript协议
    "javascript:\\s*",
    "vbscript:\\s*",
    
    -- 危险的HTML标签
    "<iframe[^>]*>", "<object[^>]*>", "<embed[^>]*>",
    "<applet[^>]*>", "<meta[^>]*>", "<link[^>]*>",
    
    -- 数据URI
    "data:text/html",
    
    -- 表达式求值
    "eval\\s*\\(", "expression\\s*\\(",
    
    -- 其他危险模式
    "document\\.(cookie|write|location)",
    "window\\.(location|open)",
    "alert\\s*\\(", "prompt\\s*\\(", "confirm\\s*\\("
}

function _M.check_xss()
    -- 检查所有用户输入
    local args = ngx.req.get_uri_args()
    for key, val in pairs(args) do
        if _M.detect_xss(val) then
            _M.block_xss("XSS in URL parameter: " .. key)
            return false
        end
    end
    
    -- 检查POST数据
    ngx.req.read_body()
    local post_args = ngx.req.get_post_args()
    if post_args then
        for key, val in pairs(post_args) do
            if _M.detect_xss(val) then
                _M.block_xss("XSS in POST parameter: " .. key)
                return false
            end
        end
    end
    
    -- 检查Cookie
    local cookies = ngx.var.http_cookie
    if cookies and _M.detect_xss(cookies) then
        _M.block_xss("XSS in Cookie")
        return false
    end
    
    -- 检查Referer
    local referer = ngx.var.http_referer
    if referer and _M.detect_xss(referer) then
        _M.block_xss("XSS in Referer")
        return false
    end
    
    return true
end

function _M.detect_xss(input)
    if not input then return false end
    if type(input) == "table" then
        for _, v in ipairs(input) do
            if _M.detect_xss(v) then
                return true
            end
        end
        return false
    end
    
    local decoded = ngx.unescape_uri(input)
    local lower = string.lower(decoded)
    
    for _, pattern in ipairs(xss_patterns) do
        if ngx.re.find(lower, pattern, "joi") then
            ngx.log(ngx.WARN, "XSS detected: ", pattern)
            return true
        end
    end
    
    return false
end

function _M.block_xss(reason)
    ngx.log(ngx.ERR, "XSS blocked: ", reason)
    ngx.status = 403
    ngx.say('{"code": 403, "msg": "XSS Attack Detected"}')
    ngx.exit(403)
end

return _M

6. 智能CC攻击防护

CC攻击是通过大量看似正常的请求来消耗服务器资源。我们实现了基于行为分析的CC攻击检测:

-- cc_defense.lua
local _M = {}
local cc_store = ngx.shared.cc_store

function _M.check_cc_attack()
    local client_ip = ngx.var.remote_addr
    local uri = ngx.var.uri
    local now = ngx.now()
    
    -- 统计请求频率
    local req_key = client_ip .. ":reqs"
    local reqs, _ = cc_store:incr(req_key, 1, 0, 60)  -- 60秒过期
    
    -- 检测异常行为
    if reqs > 100 then  -- 1分钟超过100次请求
        -- 进一步分析
        if _M.analyze_behavior(client_ip) then
            _M.block_cc(client_ip)
            return false
        end
    end
    
    return true
end

function _M.analyze_behavior(ip)
    -- 分析请求模式
    local pattern_key = ip .. ":pattern"
    local patterns = cc_store:get(pattern_key)
    
    if not patterns then
        patterns = {}
    else
        patterns = cjson.decode(patterns)
    end
    
    -- 记录请求特征
    local ua = ngx.var.http_user_agent or ""
    local uri = ngx.var.uri
    local method = ngx.var.request_method
    
    table.insert(patterns, {
        time = ngx.now(),
        uri = uri,
        method = method,
        ua_hash = ngx.md5(ua)
    })
    
    -- 只保留最近100条记录
    if #patterns > 100 then
        table.remove(patterns, 1)
    end
    
    cc_store:set(pattern_key, cjson.encode(patterns), 300)
    
    -- 检测异常模式
    local suspicious_score = 0
    
    -- 检查User-Agent变化
    local ua_set = {}
    for _, p in ipairs(patterns) do
        ua_set[p.ua_hash] = true
    end
    local ua_count = 0
    for _ in pairs(ua_set) do
        ua_count = ua_count + 1
    end
    if ua_count > 5 then  -- 短时间内使用超过5个不同UA
        suspicious_score = suspicious_score + 30
    end
    
    -- 检查请求间隔
    local intervals = {}
    for i = 2, #patterns do
        local interval = patterns[i].time - patterns[i-1].time
        table.insert(intervals, interval)
    end
    
    -- 计算间隔标准差
    if #intervals > 10 then
        local avg = 0
        for _, v in ipairs(intervals) do
            avg = avg + v
        end
        avg = avg / #intervals
        
        local variance = 0
        for _, v in ipairs(intervals) do
            variance = variance + (v - avg) ^ 2
        end
        variance = variance / #intervals
        
        if variance < 0.01 then  -- 请求间隔过于规律
            suspicious_score = suspicious_score + 40
        end
    end
    
    return suspicious_score > 50
end

function _M.block_cc(ip)
    ngx.log(ngx.ERR, "CC attack detected from: ", ip)
    -- 加入黑名单
    local blacklist = ngx.shared.blacklist
    blacklist:set(ip, true, 1800)  -- 封禁30分钟
    
    ngx.status = 503
    ngx.say('{"code": 503, "msg": "Service Temporarily Unavailable"}')
    ngx.exit(503)
end

return _M

配置文件集成

将所有模块集成到Nginx配置中:

http {
    # 定义共享内存区域
    lua_shared_dict limit_req_store 10m;
    lua_shared_dict blacklist 10m;
    lua_shared_dict whitelist 10m;
    lua_shared_dict cc_store 50m;
    
    # 初始化WAF
    init_by_lua_block {
        require "resty.core"
        cjson = require "cjson"
        
        -- 加载WAF模块
        ip_filter = require "waf.ip_filter"
        rate_limiter = require "waf.rate_limiter"
        sql_injection = require "waf.sql_injection"
        xss_filter = require "waf.xss_filter"
        cc_defense = require "waf.cc_defense"
    }
    
    server {
        listen 80;
        server_name example.com;
        
        # 访问阶段执行WAF检查
        access_by_lua_block {
            -- IP黑白名单检查
            ip_filter.check_ip()
            
            -- 速率限制
            rate_limiter.check_rate_limit()
            
            -- CC攻击防护
            cc_defense.check_cc_attack()
            
            -- SQL注入检测
            sql_injection.check_sql_injection()
            
            -- XSS攻击检测
            xss_filter.check_xss()
        }
        
        location / {
            proxy_pass http://backend;
        }
        
        # WAF管理接口
        location /waf/admin {
            content_by_lua_block {
                -- 验证管理员权限
                local auth = ngx.var.http_authorization
                if auth ~= "Bearer your-secret-token" then
                    ngx.status = 401
                    ngx.say('{"code": 401, "msg": "Unauthorized"}')
                    return
                end
                
                local method = ngx.var.request_method
                local action = ngx.var.arg_action
                
                if method == "POST" then
                    if action == "add_blacklist" then
                        local ip = ngx.var.arg_ip
                        local expire = tonumber(ngx.var.arg_expire) or 3600
                        ip_filter.add_blacklist(ip, expire)
                        ngx.say('{"code": 200, "msg": "Success"}')
                    elseif action == "add_whitelist" then
                        local ip = ngx.var.arg_ip
                        local whitelist = ngx.shared.whitelist
                        whitelist:set(ip, true)
                        ngx.say('{"code": 200, "msg": "Success"}')
                    end
                elseif method == "GET" then
                    if action == "stats" then
                        -- 返回统计信息
                        local stats = {
                            blacklist_count = ngx.shared.blacklist:count(),
                            whitelist_count = ngx.shared.whitelist:count(),
                            cc_store_count = ngx.shared.cc_store:count()
                        }
                        ngx.say(cjson.encode(stats))
                    end
                end
            }
        }
    }
}

监控与告警

一个完善的WAF系统必须配备监控和告警机制:

-- monitoring.lua
local _M = {}

function _M.log_attack(attack_type, details)
    local log_data = {
        timestamp = ngx.now(),
        type = attack_type,
        client_ip = ngx.var.remote_addr,
        uri = ngx.var.uri,
        method = ngx.var.request_method,
        user_agent = ngx.var.http_user_agent,
        details = details
    }
    
    -- 写入日志文件
    ngx.log(ngx.ERR, cjson.encode(log_data))
    
    -- 发送到监控系统
    local http = require "resty.http"
    local httpc = http.new()
    
    httpc:request_uri("http://monitoring-system/api/alert", {
        method = "POST",
        body = cjson.encode(log_data),
        headers = {
            ["Content-Type"] = "application/json",
        }
    })
end

-- 统计攻击次数
function _M.update_stats(attack_type)
    local stats = ngx.shared.stats
    local key = "attack:" .. attack_type .. ":" .. os.date("%Y%m%d")
    stats:incr(key, 1, 0, 86400)  -- 保留一天
    
    -- 检查是否需要告警
    local count = stats:get(key)
    if count > 1000 then  -- 一天超过1000次攻击
        _M.send_alert(attack_type, count)
    end
end

function _M.send_alert(attack_type, count)
    -- 实现告警逻辑,比如发送邮件或短信
    ngx.log(ngx.ALERT, "High attack volume detected: ", attack_type, " count: ", count)
end

return _M

性能优化技巧

在生产环境中,性能优化至关重要。以下是我们的优化经验:

1. 使用JIT编译优化

确保LuaJIT正常工作,可以显著提升性能:

-- 在init_by_lua_block中添加
require "resty.core"  -- 启用LuaJIT FFI加速

2. 合理使用共享内存

共享内存是OpenResty的高效数据存储方式,但需要合理规划大小:

# 根据实际需求调整大小
lua_shared_dict limit_req_store 10m;   # 存储限速信息
lua_shared_dict blacklist 10m;         # IP黑名单
lua_shared_dict whitelist 5m;          # IP白名单
lua_shared_dict cc_store 50m;          # CC攻击检测数据
lua_shared_dict stats 10m;             # 统计信息

3. 避免阻塞操作

使用非阻塞的I/O操作,比如使用cosocket API:

-- 异步发送告警
local function async_alert(data)
    ngx.timer.at(0, function()
        local http = require "resty.http"
        local httpc = http.new()
        httpc:request_uri("http://alert-service/notify", {
            method = "POST",
            body = cjson.encode(data)
        })
    end)
end

4. 规则预编译

将正则表达式预编译可以大幅提升匹配性能:

-- 初始化时编译规则
local compiled_rules = {}
for _, pattern in ipairs(raw_patterns) do
    table.insert(compiled_rules, ngx.re.compile(pattern, "joi"))
end

-- 使用时直接匹配
for _, regex in ipairs(compiled_rules) do
    if ngx.re.match(input, regex) then
        return true
    end
end

实战案例分析

让我分享一个真实的案例。去年黑色星期五期间,我们的电商平台遭受了大规模的攻击。攻击者采用了多种手段:

第一波:暴力破解攻击 攻击者尝试对登录接口进行暴力破解。我们的速率限制模块立即生效,将登录接口的请求限制在每IP每秒1次,成功阻止了99.8%的暴力破解尝试。

第二波:分布式CC攻击 攻击者使用了上千个IP地址发起CC攻击。我们的智能CC防护模块通过分析请求模式,识别出了这些伪装成正常用户的攻击流量。通过检测请求间隔的规律性和User-Agent的异常变化,系统自动将可疑IP加入了临时黑名单。

第三波:SQL注入尝试 在攻击失败后,攻击者尝试通过SQL注入获取数据库信息。我们的SQL注入检测模块成功拦截了所有注入尝试,包括一些使用了编码和混淆技术的高级注入手法。

整个攻击持续了约6小时,我们的WAF系统成功拦截了超过500万次恶意请求,保证了业务的正常运行。更重要的是,整个防护过程完全自动化,没有需要人工干预。

部署建议

基于我们的实践经验,以下是部署OpenResty WAF的建议:

1. 逐步部署 不要一次性启用所有防护规则,建议先在监控模式下运行,收集数据并调优规则,然后逐步启用拦截功能。

2. 建立白名单机制 为内部系统、监控服务、搜索引擎爬虫等建立白名单,避免误拦。

3. 定期更新规则 安全威胁不断演变,需要定期更新检测规则。建议建立规则更新机制,可以从威胁情报源自动获取最新的攻击特征。

4. 做好降级预案 虽然OpenResty性能优秀,但在极端情况下仍可能影响业务。建议准备降级开关,必要时可以快速关闭部分或全部WAF功能。

5. 日志分析与优化 定期分析WAF日志,识别误报和漏报,持续优化规则。我们使用ELK栈来收集和分析WAF日志,通过可视化dashboard实时监控攻击趋势。

总结与展望

通过OpenResty构建WAF系统,我们以极低的成本实现了企业级的Web应用防护。这套系统不仅能够有效抵御常见的Web攻击,还具有良好的扩展性和性能表现。

未来,我们计划引入机器学习技术,通过分析历史攻击数据来自动生成和优化防护规则。同时,我们也在探索将WAF与容器化部署结合,实现更灵活的安全防护架构

Last updated