我发现自己最近进入了一个以前未探索过的兔子洞,或者更具体地说,我认为是“一个”兔子洞,但实际上是一个不断扩大的兔子洞系列,这让我想到了我在这个标题中所指的发布为“6 rabbits deep”。这是一个关于防火墙、API 和层层筛选不同服务的故事,以找出看似非常良性但实际上影响很大的事情的根本原因。我们去找兔子吧!
幕后故事
当您在 Have I Been Pwned (HIBP) 上购买 API 密钥时,Stripe 会处理所有的支付魔法。我喜欢Stripe,它是一项非常棒的服务,可以消除很多痛苦,而且通过各种 API 进行集成非常简单。配置 Stripe 以通过 webhooks 将通知发送回您自己的服务也非常简单。例如,当支付发票或更新客户时,Stripe 将有关该事件的信息发送给 HIBP,然后在其门户网站的 webhooks 仪表板上列出每个调用:
可以侦听各种不同的事件并触发 webhook,这里我们只看到其中几个在名称上不言自明的事件。支付发票后,回调看起来像这样:
HIBP 已收到此电话并更新了它自己的数据库,这样对于新客户,他们现在可以检索 API 密钥,或者对于订阅已续订的现有客户,API 密钥有效期已延长。当有人升级 API 密钥时,例如从 10RPM(每分钟请求数)升级到 50RPM 时,也会发出相同的回调。 HIBP 获得回调非常重要,这样它就可以适当升级客户的密钥,并且他们可以立即开始发出更多请求。当那个电话没有发生时,好吧,让我们进入第一个兔子洞。
API 密钥升级失败?
这永远不应该发生:
这是通过HIBP 的 API 密钥支持门户提供的,非常不言自明。我检查了客户在 Stripe 上的账户,它确实显示了一个活跃的 50RPM 订阅,但是当深入研究相关的付款时,我发现了以下内容:
好吧,至少我知道哪里开始出了问题,但为什么呢?转到 webhooks 仪表板并进入失败的付款和事情看起来……次优:
该死!幸运的是,这只是所有回调的一小部分,但每次失败时,它要么阻止像上面那个人那样的人发出他们已经付费的请求,要么可能导致某人的 API 密钥过期,即使他们已经付了钱。特别是后者,我真的很担心,因为它会破坏他们的密钥,并且他们在它之上构建的任何东西都将停止运行。幸运的是,因为这是一个如此有影响力的行动,我已经为这种情况建立了缓冲堆,并且我很快就解决了这个问题,但它仍然令人不安。
那么,这是怎么回事?好吧,响应是 HTTP 403“Forbidden”,正文显然是 Cloudflare 挑战页面,所以他们端的某些东西被触发了。看来是时候去下一个兔子洞了。
Cloudflare 的防火墙和日志 ? ?
急于快速恢复功能,我进入了 Cloudflare 的 WAF 并允许所有用于 webhook 的 Stripe 出站 IP绕过它们的安全控制:
这并不理想,但它只会为来自 Stripe 的请求带来风险,并且它可以快速启动并再次运行。随着时间的推移,我现在可以更深入地研究并准确地计算出发生了什么,从日志开始。 Cloudflare 有一组非常广泛的 API,可以控制服务的大量功能,包括拉回日志(注意:这是他们的企业计划的一个功能)。我查询了与 Stripe 仪表板中某些 403 发生时间对应的日志片段,发现了 2 个与此类似的条目:
{"BotScore":1,"BotScoreSrc":"Verified Bot","CacheCacheStatus":"unknown","ClientASN":16509,"ClientCountry":"us","ClientIP":"54.187.205.235","ClientRequestHost":"haveibeenpwned.com","ClientRequestMethod":"POST","ClientRequestReferer":"","ClientRequestURI":"[redacted]","ClientRequestUserAgent":"Stripe/1.0 (+https://stripe.com/docs/webhooks)","EdgeRateLimitAction":"","EdgeResponseStatus":403,"EdgeStartTimestamp":1674073983931000000,"FirewallMatchesActions":["managedChallenge"],"FirewallMatchesRuleIDs":["6179ae15870a4bb7b2d480d4843b323c"],"FirewallMatchesSources":["firewallManaged"],"OriginResponseStatus":0,"WAFAction":"unknown","WorkerSubrequest":false}
这是 Stripe 在 54.187.205.235 上的出站 IP 之一,“FirewallMatchesRuleIDs”集合中有一个值。因此,此请求的某些内容触发了防火墙并使其受到挑战。我敢肯定,我们中的许多人之前都经历过以下思考过程:
我改变了什么?
我改变了什么吗?
他们改变了什么吗?
除了“他们”可能是 Cloudflare 或 Stripe;如果不是我干的(我很确定不是),是 Cloudflare 对规则的更改还是 Stripe 对现在触发现有规则的 webhook 负载的更改?是时候再次深入挖掘了,所以它已经转到 Cloudflare 仪表板,然后深入到 WAF 事件以请求对 webhook 回调路径的请求:
是的,有什么东西坏了!让我们深入研究一下该 IP 的近期事件:
当您通过这样的故障排除练习深入挖掘时,您会逐渐发现越来越多的信息,这些信息有助于将整个难题拼凑在一起。在这种情况下,似乎触发了“超出入站异常分数”规则。那是什么?为什么?是时候去另一个兔子洞了。
Cloudflare OWASP 核心规则集 ? ? ?
因此,我们越深入越深的兔子洞,这次深入到触发托管规则的请求的深处:
好吧,这很全面?
这里有很多东西需要解压,所以让我们从之前确定的“Inbound Anomaly Score Exceeded”规则所属的规则集开始,即 Cloudflare OWASP 核心规则集:
Cloudflare OWASP 核心规则集是 Cloudflare 对 OWASP ModSecurity 核心规则集Open external link (CRS) 的实施。 Cloudflare 根据官方代码存储库中可用的最新版本定期监视来自 OWASP 的更新。
该链接完全是另一个兔子洞,所以让我在这里简洁地总结一下:Cloudflare 使用 OWASP 的规则根据客户定义的偏执级别(您想要的严格程度)识别异常流量,然后应用分数阈值(也是客户定义的) ) 将采取行动,例如质疑请求。随着这个传奇的发展,我了解到“超出入站异常分数”规则实际上是它下面的规则的汇总。 OWASP 分数“26”是它下面列出的 6 条规则的总和,一旦超过 25,就会触发超集规则。
此外 – 这是真正重要的一点 – Cloudflare 会定期更新 OWASP 的规则,这是有道理的,因为这些规则在不断发展以应对新的威胁。他们上次升级规则是什么时候?看起来他们在我开始遇到问题之前就宣布了:
虽然上面并没有完全清楚这个版本的发布时间,但我确实联系了 Cloudflare 支持,并被告知它已经发布了:
请注意,我们确实将 OWASP 版本提升到 3.3.4,正如我们计划的更改中所指出的那样。
所以也许这不是 Cloudflare 的错或Stripe 的错,而是 OWASP 的错?公平地说,我不认为这本身是任何人的错,而只是每个人都尽最大努力将坏人拒之门外的不幸结果。除非……这真的是 Stripe 的错,因为请求有效负载中有一些东西总是可疑的,现在被捕获了?但为什么只针对某些请求而不针对其他请求?下一只兔子!
Cloudflare 有效负载记录 ? ? ? ?
有时,互联网上的人们会因为一些他们不应该做的事情而失去理智。根据我的经验,其中一件事是 Cloudflare 对流量的拦截,这是我将近 7 年前在我关于安全绝对主义的文章中详细描述的内容。 Cloudflare 在互联网生态系统中扮演着极其重要的角色,其中很大一部分价值来自于能够检查、缓存、优化,是的,甚至拒绝流量。当您使用 Cloudflare 保护您的网站时,他们会应用上述 OWASP 规则集,为了做到这一点,他们必须能够检查您的流量!但他们不记录它,不是全部记录,而只是记录他们在日志页面上引用的“我们的产品生成的元数据”。我们之前看到了一个例子,Stripe 从他们的 IP 发出的请求显示它触发了防火墙规则,但我们没有看到该 POST 请求的内容,即触发规则的实际负载。让我们去抓住它。
由于 POST 请求的内容可能包含敏感信息,因此 Cloudflare 不会记录它。显然,他们在传输过程中看到了它(这就是 OWASP 的规则可以应用于它的方式),但它没有存储在任何地方,即使你想捕获它,他们也不希望能够看到它。这就是负载日志记录(另一个企业计划功能)的用武之地,真正巧妙的是每个负载都必须使用 Cloudflare 保留的公钥加密,而只有您保留私钥。设置如下所示:
非常不言自明,一旦完成,就在我们之前看到的额外日志的下方,我们现在能够解密有效负载:
正如所承诺的,这需要之前的私钥:
现在,终于,我们有了触发规则的实际负载,在这里可以看到我自己的测试数据:
[ " },\n \"billing_reason\": \"subscription_update\",\n \"charge\": null,\n \"collection_method\": \"charge_automatically\",\n \"created\": 1674351619,\n \"currency\": \"usd\",\n \"custom_fields\": null,\n \"customer\": \"cus_MkA71FpZ7XXRlt\",\n \"customer_address\": ", " },\n \"customer_email\": \"[email protected]\",\n \"customer_name\": \"Troy Hunt 1\",\n \"customer_phone\": null,\n \"customer_shipping\": null,\n \"customer_tax_exempt\": \"none\",\n \"customer_tax_ids\": [\n\n ],\n \"default_payment_method\": null,\n \"default_source\": null,\n \"default_tax_rates\": [\n\n ],\n \"description\": \"You can manage your subscription (ie cancel it or regenerate the API key) at any time by verifying your email address here: https://haveibeenpwned.com/API/Key\",\n \"discount\": null,\n \"discounts\": [\n\n ],\n \"due_date\": null,\n \"ending_balance\": -11804,\n \"footer\": null,\n \"from_invoice\": null,\n \"hosted_invoice_url\": \"https://invoice.stripe.com/i/acct_1EdQYpEF14jWlYDw/test_YWNjdF8xRWRRWXBFRjE0aldsWUR3LF9OREo5SlpqUFFvVnFtQnBVcE91YUFXemtkRHFpQWNWLDY0ODkyNDIw02004bEyljdC?s=ap\",\n \"invoice_pdf\": \"https://pay.stripe.com/invoice/acct_1EdQYpEF14jWlYDw/test_YWNjdF8xRWRRWXBFRjE0aldsWUR3LF9OREo5SlpqUFFvVnFtQnBVcE91YUFXemtkRHFpQWNWLDY0ODkyNDIw02004bEyljdC/pdf?s=ap\",\n \"last_finalization_error\": null,\n \"latest_revision\": null,\n \"lines\": ", " ", " ],\n \"discountable\": false,\n \"discounts\": [\n\n ],\n \"invoice_item\": \"ii_1MSsXfEF14jWlYDwB1nfZvFm\",\n \"livemode\": false,\n \"metadata\": ", " },\n \"period\": ", " },\n \"plan\": ", " },\n \"nickname\": null,\n \"product\": \"prod_Mk4eLcJ7JYF02f\",\n \"tiers_mode\": null,\n \"transform_usage\": null,\n \"trial_period_days\": null,\n \"usage_type\": \"licensed\"\n },\n \"price\": ", " },\n \"nickname\": null,\n \"product\": \"prod_Mk4eLcJ7JYF02f\",\n \"recurring\": ", " },\n \"tax_behavior\": \"unspecified\",\n \"tiers_mode\": null,\n \"transform_quantity\": null,\n \"type\": \"recurring\",\n \"unit_amount\": 15000,\n \"unit_amount_decimal\": \"15000\"\n },\n \"proration\": true,\n \"proration_details\": ", " \"il_1MMjfcEF14jWlYDwoe7uhDPF\"\n ]\n }\n },\n \"quantity\": 1,\n \"subscription\": \"sub_1MMjfcEF14jWlYDwi8JWFcxw\",\n \"subscription_item\": \"si_N6xapJ8gSXdp7W\",\n \"tax_amounts\": [\n\n ],\n \"tax_rates\": [\n\n ],\n \"type\": \"invoiceitem\",\n \"unit_amount_excluding_tax\": \"-14304\"\n },\n ", " ],\n \"discountable\": true,\n \"discounts\": [\n\n ],\n \"livemode\": false,\n \"metadata\": ", " },\n \"period\": ", " },\n \"plan\": ", " },\n \"nickname\": null,\n \"product\": \"prod_Mk4lTSl4axd9mt\",\n \"tiers_mode\": null,\n \"transform_usage\": null,\n \"trial_period_days\": null,\n \"usage_type\": \"licensed\"\n },\n \"price\": ", " },\n \"nickname\": null,\n \"product\": \"prod_Mk4lTSl4axd9mt\",\n \"recurring\": ", " },\n \"tax_behavior\": \"unspecified\",\n \"tiers_mode\": null,\n \"transform_quantity\": null,\n \"type\": \"recurring\",\n \"unit_amount\": 2500,\n \"unit_amount_decimal\": \"2500\"\n },\n \"proration\": false,\n \"proration_details\": ", " },\n \"quantity\": 1,\n \"subscription\": \"sub_1MMjfcEF14jWlYDwi8JWFcxw\",\n \"subscription_item\": \"si_NDJ98tQrCcviJf\",\n \"tax_amounts\": [\n\n ],\n \"tax_rates\": [\n\n ],\n \"type\": \"subscription\",\n \"unit_amount_excluding_tax\": \"2500\"\n }\n ],\n \"has_more\": false,\n \"total_count\": 2,\n \"url\": \"/v1/invoices/in_1MSsXfEF14jWlYDwxHKk4ASA/lines\"\n },\n \"livemode\": false,\n \"metadata\": ", " },\n \"next_payment_attempt\": null,\n \"number\": \"04FC1917-0008\",\n \"on_behalf_of\": null,\n \"paid\": true,\n \"paid_out_of_band\": false,\n \"payment_intent\": null,\n \"payment_settings\": ", " },\n \"period_end\": 1674351619,\n \"period_start\": 1674351619,\n \"post_payment_credit_notes_amount\": 0,\n \"pre_payment_credit_notes_amount\": 0,\n \"quote\": null,\n \"receipt_number\": null,\n \"rendering_options\": null,\n \"starting_balance\": 0,\n \"statement_descriptor\": null,\n \"status\": \"paid\",\n \"status_transitions\": ", " },\n \"subscription\": \"sub_1MMjfcEF14jWlYDwi8JWFcxw\",\n \"subtotal\": -11804,\n \"subtotal_excluding_tax\": -11804,\n \"tax\": null,\n \"test_clock\": null,\n \"total\": -11804,\n \"total_discount_amounts\": [\n\n ],\n \"total_excluding_tax\": -11804,\n \"total_tax_amounts\": [\n\n ],\n \"transfer_data\": null,\n \"webhooks_delivered_at\": 1674351619\n }\n },\n \"livemode\": false,\n \"pending_webhooks\": 1,\n \"request\": ", " },\n \"type\": \"invoice.paid\"\n}" ]
但是有效载荷中存在的东西已经够多了,尤其让我印象深刻的是缺少的东西。没有明显的 XSS 模式,也没有 SQL 注入或任何其他看起来可疑的字符串。这个请求看起来完全是良性的,那么它为什么会触发规则呢?
我想将被阻止请求的负载与未被阻止的类似请求进行比较,但它们仅在触发规则时才会记录在 Cloudflare 中。没问题,很容易从 Stripe 的 webhook 历史中获取完整的请求,所以我找到了一个通过的请求和一个失败的请求,并对它们进行了比较:
这显然不是完整的 200 行,但它是文件其余部分的一个非常相似的故事;微小的差异主要归结于日期、ID,当然还有客户本身。没有可疑的图案,没有时髦的字符,没有明显的异常。甚至提及它也有点毫无意义,因为它们几乎相同,但左侧的有效载荷是通过防火墙的有效载荷,而右侧的有效载荷被阻止了。
下一个兔子洞!
Cloudflare 的内部规则引擎 ? ? ? ? ?
完全没有想法和选择,焦点转移到 Cloudflare 内部的人们身上,他们已经意识到存在问题:
我们正在积极调查此事,并可能很快发布 Cloudflare OWASP 规则集的更新
– Michael Tremante (@MichaelTremante) 2023 年 1 月 20 日
接下来是一段最初与 Cloudflare 来来回回的时期,然后是 Stripe 以及每个人都试图找出问题所在的确切位置。本质上,这个过程是这样的:
Cloudflare 是否无意中阻止了请求?
OWASP 规则集是否会引发误报?
Stripe 是否发出被认为是恶意的请求?
我们走了一圈又一圈。有一次,Cloudflare 发现 OWASP 规则集发生了变化,这似乎导致其实施无意中触发了 WAF。他们把它滚回去……同样的事情发生了。我们推迟回 Stripe 假设他们一定发生了一些变化,但他们无法确定任何会产生任何实质性影响的变化。我们被难住了,但我们也有一个简单的解决办法,就在最后一个兔子洞之外……
微调 Cloudflare WAF ? ? ? ? ? ?
托管防火墙的乐趣在于,其他人可以免去照料它的所有繁琐工作。我将在简短但明确的摘要中详细讨论这一点,这也会产生风险,因为您将流量控制委托给其他人。幸运的是,Cloudflare 通过其托管规则为您提供了大量的可配置性,使您可以轻松添加自定义异常:
这意味着我可以创建一个简单的异常,它比之前的“只让所有出站 Stripe IP 进入”更智能,方法是向下过滤到那些 webhook 流入的特定路径:
最后,因为顺序很重要,我将该规则拖到堆的顶部,这样它会导致匹配的入站请求跳过所有其他规则:
最后,没有更多的兔子?
得到教训
我知道你在想什么——“真正的根本原因是什么?” – 老实说,我还是不知道。我不知道它是 Cloudflare、OWASP 还是 Stripe,或者它是否影响了这些服务的其他客户,老实说,是的,这有点令人沮丧。但我学到了很多东西,仅此一项,我就从中吸取了三大教训:
首先,了解所有这些位如何协同工作的管道非常重要。我很幸运,这不是一个时间紧迫的问题,我有幸在不受胁迫的情况下学习;规则、有效负载检查和异常管理如何协同工作是非常值得理解的东西。就这样,好像是为了强调我的第一点,我在点击博文上的发布按钮之前发现了这一点:
我在 Cloudflare 的异常中添加了更多的 OWASP 规则(比如添加 5 点的 MySQL 规则),然后我们又回到了业务中。
其次,我比以前更看好 Cloudflare 提供的托管 WAF,因为我对它的全面性有了更好的了解。我想编写代码并在 Web 上运行应用程序,这是我的重点,我希望其他人在上面提供额外的层,不断适应以阻止新出现的威胁。我想了解它(我现在明白了,至少肯定比以前更好),但我不想日复一日地管理它成为我的工作。
最后,恕我直言,Stripe 需要一个更好的机制来报告 webhook 失败:
在实时模式下,您会在尝试 3 天后收到通知。您还可以查询事件 ( https://t.co/0mujOPssV0 ),以在已发送的 Web 挂钩上创建状态运行列表,并通过您自己的应用程序对此发出警报。
– Blake Krone (@blakekrone) 2023 年 1 月 19 日
等到东西坏了真的不理想,虽然我确定你可以插入 Stripe 拥有的(非常广泛的)API 生态系统,但这对他们来说是一个很容易构建的功能。所以,Stripe 的朋友们,当你阅读这篇文章时对于某种形式的异常 webhook 响应警报,这是我投的一个大大的“赞成”票。
这种体验既有挫折感又有乐趣,虽然前者可能很明显,但后者仅仅是因为有机会学习一些新东西,这是我经营的服务中非常重要的一部分。如果你遇到同样的问题,愿我在这里沮丧的有趣故事让你的生活更轻松?
原文: https://www.troyhunt.com/down-the-cloudflare-stripe-owasp-rabbit-hole-a-tale-of-6-rabbits-deep/