提升WAF可见性:深入解析有效载荷日志记录技术

本文详细介绍了Cloudflare如何通过改进有效载荷日志记录功能,帮助客户精准了解Web应用防火墙的拦截原因,包括其技术原理、引擎优化、上下文增强以及未来的发展方向,显著降低了误报排查的复杂度。

通过有效载荷日志记录提升WAF的可见性

随着网络攻击面的扩大,Cloudflare的Web应用防火墙提供了多种解决方案来缓解这些攻击。这对我们的客户来说是件好事,但我们服务的数百万请求工作负载的基数意味着生成误报是不可避免的。这意味着我们必须为客户调整默认配置。

调整配置并非一个不透明的过程:客户需要获取一些数据点,然后决定什么对他们有效。本文解释了我们所提供的技术,旨在让客户了解WAF采取某些行动的原因——以及为了减少噪音、增强信号所做的改进。

日志操作很棒——但我们还能做得更多吗?

Cloudflare的WAF保护源服务器免受各种第7层攻击,这些攻击针对的是应用层。防护通过各种工具实现,例如:

  • 托管规则:由Cloudflare的安全分析师编写,用于解决常见漏洞与暴露、OWASP安全风险以及Log4Shell等漏洞。
  • 自定义规则:客户可以使用强大的规则语言编写规则。
  • 速率限制规则、恶意上传检测、凭据泄露检测等。

这些工具构建在规则集引擎之上。当规则表达式匹配时,引擎会执行一个操作。 日志操作用于模拟规则的行为。此操作证明规则表达式被引擎匹配,并发出一个日志事件,可以通过安全分析、安全事件、日志推送或边缘日志交付访问。 日志非常擅长验证规则在预期匹配的流量上是否按预期工作,但仅显示规则匹配还不够,特别是当规则表达式可能包含许多代码路径时。

在伪代码中,一个表达式可能如下所示:

1
如果任何HTTP请求头包含"authorization"键 或 HTTP主机头的lowercase表示以"cloudflare"开头 则记录

规则语言的语法将是:

1
any(http.request.headers[*] contains "authorization") or starts_with(lower(http.host), "cloudflare")

调试这个表达式会带来几个问题。是上面OR表达式的左侧还是右侧匹配的?像Base64解码、URL解码,以及本例中的小写转换这样的函数,可以对字段的原始表示应用转换,这导致了对请求的哪些特性导致匹配存在进一步的模糊性。

更复杂的是,规则集中的许多规则都可以注册匹配。像Cloudflare OWASP这样的规则集使用不同规则的累计分数,在分数超过设定阈值时触发操作。 此外,Cloudflare托管规则和OWASP规则的表达式是私有的。这增强了我们的安全态势,但也意味着客户只能从标题、标签和描述中猜测这些规则的作用。例如,一个规则可能被标记为"SonicWall SMA - Remote Code Execution - CVE:CVE-2025-32819"。

这就引出了问题:我请求的哪个部分导致了规则集引擎中的匹配?这些是误报吗? 这正是有效载荷日志记录发挥作用的地方。它可以帮助我们追溯到导致匹配的规则中,经过转换后的具体字段及其各自的值。

有效载荷日志记录

有效载荷日志记录是一项记录请求中哪些字段与导致WAF采取行动的规则相关联的功能。这减少了模糊性,并提供了有用的信息,可以帮助发现误报、保证正确性,并有助于微调这些规则以获得更好的性能。

根据上面的例子,一个有效载荷日志条目将包含表达式的左侧或右侧,但不会同时包含两者。

有效载荷日志记录是如何工作的?

有效载荷日志记录和规则集引擎构建在Wirefilter之上,这已被广泛解释。 从根本上说,这些引擎是用Rust编写的对象,它们实现了一个编译器特征。这个特征驱动着从这些表达式派生的抽象语法树的编译。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct PayloadLoggingCompiler {
     regex_cache HashMap<String, Arc<Regex>>
}

impl wirefilter::Compiler for PayloadLoggingCompiler {
	type U = PayloadLoggingUserData
	
	fn compile_logical_expr(&mut self, node: LogicalExpr) -> CompiledExpr<Self::U> {
		// ...
		let regex = self.regex_cache.entry(regex_pattern)
		.or_insert_with(|| Arc::new(regex))
		// ...
	}

}

规则集引擎执行一个表达式,如果其评估结果为真,则该表达式及其执行上下文将被发送到有效载荷日志记录编译器进行重新评估。执行上下文提供了评估表达式所需的所有运行时值。 重新评估完成后,将记录表达式评估结果为真的分支中涉及的字段。

日志的结构是一个wirefilter字段及其值的映射 Map<Field, Value>

1
2
3
4
5
{
	“http.host”: “cloudflare.com”,
	“http.method”: “get”,
	“http.user_agent”: “mozilla”
}

注意:这些日志使用客户提供的公钥进行加密。 这些日志经过我们的日志处理管道,可以通过多种方式读取。客户可以配置一个日志推送作业,将其写入我们构建的自定义Worker,该Worker使用客户的私钥自动解密这些日志。有效载荷日志记录CLI工具、Worker或Cloudflare仪表板也可用于解密。

我们发布了哪些改进?

在wirefilter中,有些字段是数组类型。字段http.request.headers.names是一个包含请求中所有头名称的数组。例如:

1
[“content-type”, “content-length”, “authorization”, "host"]

一个读取 any(http.request.headers.names[*] contains “c”) 的表达式将评估为真,因为至少有一个头包含字母“c”。在之前版本的有效载荷日志记录编译器中,http.request.headers.names 字段中的所有头都将被记录,因为它是评估为真的表达式的一部分。 有效载荷日志

1
http.request.headers.names[*] = [“content-type”, “content-length”, “authorization”, "host"]

现在,我们对数组字段进行部分评估,并记录匹配表达式约束的索引。在这种情况下,将只记录包含“c”的头部! 有效载荷日志

1
http.request.headers.names[0,1] = [“content-type”, “content-length”]

操作符

这就引出了wirefilter中的操作符。一些像“eq”这样的操作符会产生精确匹配,例如 http.host eq “a.com”。还有其他一些操作符会产生“部分”匹配——例如“in”、“contains”、“matches”——这些通常与正则表达式一起使用。

本例中的表达式:any(http.request.headers[*] contains “c”) 使用了“contains”操作符,它会产生部分匹配。它还使用了“any”函数,我们可以说它也会产生部分匹配,因为如果至少有一个头包含“c”,那么我们应该记录那个头——而不是像之前版本那样记录所有头。

随着有效载荷日志记录编译器的改进,当这些表达式被评估时,我们只记录部分匹配。在这种情况下,新的有效载荷日志记录编译器处理“contains”操作符的方式类似于Rust标准库中字节的“find”方法。这将我们的有效载荷日志改进为:

1
http.request.headers.names[0,1] = [“c”, “c”]

这让事情清晰了很多。它还为我们的日志处理管道节省了数百万字节的处理量。例如,一个被大量分析的字段是请求体——http.request.body.raw——它可能有几十千字节大小。有时表达式检查的是一个应匹配三个字符的正则表达式模式。在这种情况下,我们将只记录3个字节,而不是几千字节!

上下文

我知道,我知道,[“c”, “c”] 并没有太多意义。即使我们已经提供了匹配的确切原因,并显著减少了写入客户存储目的地的字节量,关键目标是为客户提供有用的调试信息。作为有效载荷日志记录改进的一部分,编译器现在还记录部分匹配的“之前”和“之后”内容。这些缓冲区的大小目前各为15个字节。这意味着我们的有效载荷日志现在看起来像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
http.request.headers[0,1] = [
    {
        before: null, // 不包含在最终日志中
        content: “c”, 
        after: “ontent-length”
    },
    {
        before: null, // 不包含在最终日志中
        content: “c”, 
        after:”ontent-type”
    }
]
  • 先前有效载荷日志示例
    • (图片说明:旧版日志示例,显示所有请求头和值)
  • 新的有效载荷日志示例
    • (图片说明:新版日志示例,突出显示了匹配的特定部分和上下文)

在之前的日志中,我们拥有所有的头值。在新的日志中,我们有第8个索引,它是一个HTTP头中的恶意脚本。匹配发生在" comments powered by Disqus