蓝易云CDN:Web应用防火墙配置CC攻击防护规则防御CC攻击

蓝易云CDN:Web应用防火墙配置CC攻击防护规则

CC攻击的本质是用看似合法的HTTP请求淹没服务器的应用层处理能力。攻击者不需要海量带宽,只需要持续不断地向动态接口发起高频请求,就能让后端数据库连接池耗尽、PHP-FPM进程占满、Java线程池阻塞,最终整个站点陷入无响应状态 ⚠️

蓝易云CDN的WAF模块基于OpenResty构建,CC防护规则直接运行在边缘节点的Lua层,在恶意请求到达源站之前就完成识别和拦截。下面从规则设计逻辑到具体配置方法,完整讲解如何在WAF中构建一套多维度的CC防护体系。


一、WAF中CC防护规则的设计思路

有效的CC防护不能只靠单一维度判断,需要从请求频率、访问路径、客户端特征、行为模式四个层面建立规则矩阵,多条规则协同工作才能兼顾防护精度和业务兼容性 🎯

核心原则:宁可分层递进处置,也不要一刀切封禁。第一层限速告警,第二层JS验证,第三层临时封禁——逐级升级响应力度,把误伤控制在最低。

二、基于URI路径的精细化限速规则

不同路径承受CC攻击的风险完全不同。静态资源几乎不消耗后端资源,而登录、搜索、支付等动态接口每次请求都涉及数据库操作。WAF规则必须按路径分级设置限速阈值:

http {
    # 动态API接口 - 严格限速
    limit_req_zone $binary_remote_addr zone=api_zone:20m rate=10r/s;
    # 页面浏览 - 适度限速
    limit_req_zone $binary_remote_addr zone=page_zone:15m rate=40r/s;
    # 静态资源 - 宽松限速
    limit_req_zone $binary_remote_addr zone=static_zone:10m rate=120r/s;
}

解释: 这里定义了三个独立的限速区域,每个区域使用客户端IP地址($binary_remote_addr,二进制格式占用更少内存)作为限速键。zone=api_zone:20m 表示分配20MB共享内存用于存储IP访问状态,足以同时追踪约16万个不同IP。rate=10r/s 表示API接口每个IP每秒只允许10次请求,而静态资源放宽到120次。三个区域完全独立计数互不干扰。

接下来在对应的location中启用:

server {
    location /api/ {
        limit_req zone=api_zone burst=15 nodelay;
        limit_req_status 429;
        proxy_pass http://backend;
    }

    location ~ \.(php|jsp|do)$ {
        limit_req zone=page_zone burst=30 nodelay;
        limit_req_status 429;
        proxy_pass http://backend;
    }

    location ~* \.(css|js|png|jpg|gif|ico|woff2)$ {
        limit_req zone=static_zone burst=80;
        proxy_pass http://backend;
    }
}

解释: burst=15 允许在短时间内积攒最多15个突发请求,超过速率限制的请求进入这个缓冲队列。nodelay 指示队列中的请求立即处理而非均匀延迟释放,超出burst容量的请求直接被拒绝。limit_req_status 429 将响应码设为429(Too Many Requests),这是HTTP标准中专门用于限速场景的状态码,便于客户端和监控系统准确识别 📊

三、基于请求特征的WAF识别规则

CC攻击工具的HTTP请求往往在头部字段上暴露出明显破绽。WAF可以针对这些特征建立过滤规则:

-- WAF CC特征识别模块
access_by_lua_block {
    local headers = ngx.req.get_headers()
    local ua = headers["User-Agent"] or ""
    local referer = headers["Referer"] or ""
    local accept_lang = headers["Accept-Language"] or ""

    -- 规则1: 空UA或已知攻击工具UA
    local bad_ua_patterns = {
        "^$",                    -- 空User-Agent
        "python%-requests",      -- Python requests库
        "Go%-http%-client",      -- Go默认HTTP客户端
        "curl/",                 -- curl命令行工具
        "ab/%d",                 -- Apache Bench压测工具
        "wrk",                   -- wrk压测工具
        "siege"                  -- siege压测工具
    }

    for _, pattern in ipairs(bad_ua_patterns) do
        if ngx.re.find(ua, pattern, "ijo") then
            ngx.log(ngx.WARN, "CC-WAF: bad UA blocked, IP=",
                    ngx.var.remote_addr, " UA=", ua)
            return ngx.exit(403)
        end
    end

    -- 规则2: 缺少Accept-Language头
    -- 真实浏览器几乎都会发送此头部
    if #accept_lang == 0 and ngx.re.find(ua, "Mozilla", "jo") then
        ngx.var.cc_suspicious = "1"
    end
}

逐段解释:

  • 首先通过 ngx.req.get_headers() 获取完整的请求头表,分别提取UA、Referer和Accept-Language字段。
  • bad_ua_patterns 列表中定义了多个已知的自动化工具标识。python%-requests 中的 %- 是Lua正则中对连字符的转义,匹配Python requests库的默认UA。
  • ngx.re.find(ua, pattern, "ijo") 使用Nginx正则引擎进行匹配,i 表示不区分大小写,j 启用PCRE JIT编译加速,o 表示只编译一次正则表达式缓存复用。
  • 规则2的逻辑是:如果请求声称自己是Mozilla系浏览器(UA中包含Mozilla字样),却没有发送Accept-Language头部,这在真实浏览器中几乎不会出现,标记为可疑请求进入下一步验证 🔍

四、动态处置——Cookie验证与滑动窗口封禁

仅依靠静态规则拦截远远不够,WAF需要对可疑请求执行动态验证,并对确认的攻击源实施自动封禁:

access_by_lua_block {
    local limit = ngx.shared.waf_cc_dict

    -- 阶段一:滑动窗口计数
    local ip = ngx.var.remote_addr
    local uri = ngx.var.uri
    local key = ip .. ":" .. uri
    local count, err = limit:incr(key, 1, 0, 60)

    -- 阶段二:超过阈值触发Cookie验证
    if count > 120 then
        local token = ngx.var.cookie_waf_cc_token
        local expected = ngx.md5(ip .. "lanyiyun_salt_2026" .. os.date("%Y%m%d%H"))

        if token ~= expected then
            ngx.header["Content-Type"] = "text/html; charset=utf-8"
            ngx.header["Cache-Control"] = "no-store"
            ngx.say(string.format([[
                <html><body>
                <script>
                (function(){
                    var t = "%s";
                    document.cookie = "waf_cc_token=" + t
                        + ";path=/;max-age=3600;SameSite=Lax";
                    setTimeout(function(){ location.reload(); }, 100);
                })();
                </script>
                <noscript>请启用JavaScript后刷新页面</noscript>
                </body></html>
            ]], expected))
            return ngx.exit(200)
        end
    end

    -- 阶段三:验证后仍然高频 - 临时封禁
    if count > 500 then
        local block_key = "blocked:" .. ip
        limit:set(block_key, 1, 300)  -- 封禁5分钟
        ngx.log(ngx.ERR, "CC-WAF: IP blocked for 300s, IP=", ip,
                " count=", count, " uri=", uri)
        return ngx.exit(444)
    end

    -- 检查是否在封禁名单中
    local blocked = limit:get("blocked:" .. ip)
    if blocked then
        return ngx.exit(444)
    end
}

分阶段解释:

阶段一 使用OpenResty共享内存字典 waf_cc_dict 进行滑动窗口计数。limit:incr(key, 1, 0, 60) 的含义是:对指定key加1,初始值为0,生存时间60秒。也就是统计每个IP在60秒内对每个URI的访问次数,60秒后自动清零重新计算。

阶段二 当60秒内同一IP对同一URI的请求超过120次时,触发Cookie验证。服务端计算一个基于IP、密钥和当前小时数的MD5值作为预期token,返回一段JS让浏览器设置Cookie后刷新。真实浏览器会自动完成验证并携带正确Cookie继续访问,攻击脚本无法执行JS则会被持续拦截在验证环节 ✅

阶段三 如果某个IP即便通过了Cookie验证,60秒内请求仍然超过500次,说明这是具备JS执行能力的高级攻击工具(如Puppeteer、Playwright等),直接写入封禁名单,有效期300秒(5分钟),期间所有请求以444状态码直接断开连接 🚫

五、WAF规则调优的关键要点 ⚡

共享内存容量规划——waf_cc_dict 的大小直接决定能同时跟踪多少个IP+URI的访问状态。建议至少分配32MB,按每条记录约200字节计算,可支撑约17万条并发跟踪记录。在 nginx.conf 的http段声明:

lua_shared_dict waf_cc_dict 32m;

解释: 这条指令在Nginx的worker进程之间创建一块32MB的共享内存区域,所有worker进程读写同一份数据,确保限速计数在多进程间保持一致。

真实IP获取必须前置处理——CDN多级转发场景下,ngx.var.remote_addr 拿到的可能是上游代理节点的IP。必须在WAF逻辑之前通过 set_real_ip_from 指令或Lua代码从 X-Forwarded-For 头中提取真实客户端地址,否则所有规则的IP维度判断都会失准。

分时段动态调整阈值——业务高峰期正常用户的请求频率本身就较高,固定不变的阈值容易造成误封。建议在WAF规则中加入时段判断逻辑,高峰时段适当放宽限速上限,低谷时段收紧,保持防护效果的同时最大程度减少对正常访问的影响 📋


WAF层面的CC防护核心在于分层递进、动态响应。从路径级限速到请求特征过滤,再到Cookie人机验证和自动封禁,四道防线逐级收紧。蓝易云CDN将这套规则体系部署在边缘节点的OpenResty/Lua层,每一次恶意请求都在距离源站最远的位置被拦截,从而确保业务服务器始终保持充裕的处理余量 🛡️

THE END