WAF 自定义动作

WAF 自定义动作在请求满足配置的 WAF 动作触发条件时调用全局 Lua 模块。模块接收 WAF 判定结果,可以返回响应、展示自己的挑战页面,或者继续处理请求。

WAF 自定义动作在请求满足配置的 WAF 动作触发条件时调用全局 Lua 模块。模块接收 WAF 判定结果,可以返回响应、展示自己的挑战页面,或者继续处理请求。

配置步骤

  1. 创建一个全局 Lua 模块,按下文约定导出相应回调,其中只有 invoke 是必需的。

    新建 Lua 模块表单及示例 WAF 动作模块

  2. 在应用的页面规则 WAF 配置中,将拦截动作设为自定义动作,再在模块名中选择该模块。

  3. 配置 WAF 规则偏执级别敏感级别,然后保存并发布页面规则变更。

    拦截动作选择自定义动作后的 WAF 配置

请求已有有效的 Edge 挑战验证放行凭证时,会跳过此动作。

invoke 回调

Edge 以普通函数形式调用 invoke(params),不传入隐含的 self 参数。params 是 Lua table,不是 JSON 字符串,也没有额外的请求上下文参数。

字段Lua 类型说明
timenumber挑战创建时间,Unix 时间戳,精度为整秒。
module_namestring选定的全局 Lua 模块名称。
clearance_timenumbernil验证成功后的放行时长,单位为秒。管理后台的自定义动作没有该配置项,因此只有通过管理 API 配置规则时才有值,否则为 nil
captcha_typestring固定为 "custom"
tokenstring已加密、Base64 编码并经过 URI 转义的 token,用于后续挑战请求。不要自行解析其内容。
prev_urlstring原始请求 URI,包含查询参数,已经过 URI 转义。
verdicttableWAF 判定结果,见下文。

WAF 判定结果

字段Lua 类型说明
scorenumber此次动作判定使用的分数;开启跨请求模式时为累计分数。显式 WAF 判定可能使用特殊分值,不应假设分数始终为正数。
actionstring固定为 "custom-action",即拦截动作选择自定义动作时 Edge 使用的取值。
thresholdnumbernil触发该动作的分数阈值,由敏感级别最低分数设定;没有阈值时可缺省。
hit_types字符串数组命中的规则分组,不应依赖其排列顺序。
rule_setstablenil规则集 ID 到 "分数/阈值" 字符串的映射,仅包含达到各自阈值的规则集。它不是所有命中规则集的列表,可能缺省,开启跨请求模式时也可能没有此字段。
rulestable 数组命中的规则;每项包含 rule_name(字符串,未设置时可缺省)、rule_id(数值)和 group(字符串)。

例如,rule_sets 中的 [12] = "10/5" 表示规则集 12 的分数为 10,阈值为 5。rules 不包含单条规则的分数、命中的请求数据或完整 WAF 日志。请将判定结果视为只读数据,并处理可选字段缺省的情况。

返回值

返回值行为
content, status以数值型 HTTP 状态码返回响应体并结束请求,默认内容类型为 text/html
content以 HTTP 403 返回响应体并结束请求。
nil, "pass"继续处理请求,不发送响应,也不签发放行 cookie。其他规则仍可能影响此请求。
nilnil, 429因为没有提供响应体,返回 HTTP 403。

需要空响应体时,返回空字符串,例如 return "", 429。应将响应体和状态码返回给 Edge,不要先输出响应体再返回。找不到模块或模块没有导出 invoke 时,返回 HTTP 500。

示例:返回 HTTP 429

以下模块返回静态拦截页面,不实现挑战验证,也不签发放行凭证:

local _M = {}

local function invoke(params)
    return [[<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Request temporarily blocked</title></head>
<body><h1>Request temporarily blocked</h1><p>Please try again later.</p></body>
</html>]], 429
end
_M.invoke = invoke

return _M

自定义挑战页面同样可以返回挑战 HTML 和 429。这样会在生成响应时直接设置状态码,无需在响应头过滤阶段将 403 改成 429。此返回值仅影响本次自定义响应,不会改变内置拦截动作、内置挑战或挑战验证失败的状态码。

如果模块自行检查后决定放行当前请求,返回 nil, "pass"。这不会为后续请求授予放行凭证。

挑战回调

仅拦截或放行请求的模块只需导出 invoke。要实现完整的挑战流程——返回挑战页面、校验答案,并在一段时间内放行该客户端——还需导出 verify;如果挑战页面需要单独获取挑战内容,再导出 create

Edge 将两个固定入口路由到这两个回调。它们都通过 invoke 收到的 token 识别模块,因此挑战页面必须把该 token 传递下去。

这两个回调的 params 都从 token 解码得到,包含 timemodule_nameclearance_timecaptcha_typeverdict,不包含 Edge 为 invoke 额外添加的 tokenprev_url。传给 invoketokenprev_url 已经过 URI 转义,拼入下文的查询参数或表单请求体时不要再次编码。

create

GET /.edge-waf/create-captcha?token=TOKEN

Edge 调用 create(params, uri_args)

参数Lua 类型说明
paramstable从 token 解码得到的数据,见上文。
uri_argstable解析后的查询参数,包含 token。使用其他客户端传入的值前应先校验。
返回值行为
非空字符串作为响应体以 HTTP 200 返回。
nil""返回 HTTP 403。

第二个返回值会被忽略,状态码只会是 200 或 403。Edge 不会为该响应设置 Content-Type,如果客户端需要,请在回调中用 ngx.header.content_type 设置。

请求该入口时如果缺少 token 或 token 无法解码,请求不会到达模块,而是回退到 Edge 内置的验证码图片。token 能够解码、但模块没有导出 create 时,返回 HTTP 500。

verify

POST /.edge-waf/edge-recaptcha
Content-Type: application/x-www-form-urlencoded

请求体必须原样携带 invoke 中的 tokenprev_url,以及自定义的答案字段。缺少其中任一个时,Edge 返回 HTTP 403。

Edge 调用 verify(params, post_args)

参数Lua 类型说明
paramstable从 token 解码得到的数据,见上文。
post_argstable解析后的表单字段。信任客户端提交的答案前应先校验。
返回值行为
trueEdge 签发放行凭证,并以 HTTP 200 返回 URI 反转义后的 prev_url 作为响应体,由页面据此跳转。
falsenil返回 HTTP 403。

放行凭证是有效期为 params.clearance_time 秒的 waf-verify cookie;未配置放行时长时为 60 秒,管理后台配置的规则即属于这种情况。该 cookie 有效期内会完全跳过此 WAF 动作,因此不会再次调用 invoke。模块没有导出 verify 时,返回 HTTP 500。

示例:完整的挑战流程

invoke 返回携带 token 的页面,页面向 create 获取题目,verify 校验答案并签发放行凭证:

local _M = {}

local str_fmt = string.format

local PAGE = [[<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Checking your request</title></head>
<body>
<p id="question">Loading...</p>
<input id="answer"><button onclick="send()">Continue</button>
<script>
var token = "%s", prevUrl = "%s";

fetch("/.edge-waf/create-captcha?token=" + token)
    .then(function (r) { return r.json(); })
    .then(function (c) { document.getElementById("question").textContent = c.question; });

function send() {
    fetch("/.edge-waf/edge-recaptcha", {
        method: "POST",
        headers: {"Content-Type": "application/x-www-form-urlencoded"},
        body: "token=" + token + "&prev_url=" + prevUrl
              + "&answer=" + encodeURIComponent(document.getElementById("answer").value)
    }).then(function (r) {
        if (r.status === 200) {
            r.text().then(function (url) { location.replace(url); });
        }
    });
}
</script>
</body>
</html>]]

local function invoke(params)
    return str_fmt(PAGE, params.token, params.prev_url), 429
end
_M.invoke = invoke

local function create(params, uri_args)
    ngx.header.content_type = "application/json"

    return [[{"question": "What is two plus three?"}]]
end
_M.create = create

local function verify(params, post_args)
    if post_args.answer ~= "5" then
        return false
    end

    return true
end
_M.verify = verify

return _M

模块也可以不使用 Edge 的入口,而是自建挑战入口:在页面规则中用 foreign-call() 把自定义 URI 路由到 createverify,自行管理放行 cookie,并在该 cookie 存在时让 invoke 返回 nil, "pass"。这样调用时,回调只会收到规则传入的参数,而不是 Edge 的 params,需要自行读取请求内容。

在 Edgelang 挑战接口中使用这三个回调的说明和验证码实现示例,参见自定义挑战