可动态重配置的CORS中间件:jub0bs/cors的技术革新与安全实践

本文详细介绍了jub0bs/cors中间件库如何实现CORS配置的动态重配置功能,包括Reconfigure和Config方法的实现原理、并发安全性保障,以及与rs/cors的对比分析,同时披露了早期版本中的关键安全漏洞及修复过程。

可动态重配置的CORS中间件与jub0bs/cors

TL;DR
在这篇对之前文章的简短跟进中,我描述了为什么以及如何在jub0bs/cors中添加了对CORS中间件动态重配置的支持。

重新思考配置不可变性

直到最近,我一直强烈主张CORS中间件不应在运行时重新配置,任何配置更改都需要服务器重启:

由于CORS放宽了同源策略(SOP)强制执行的一些限制,它是一个安全关键机制。因此,服务器的CORS配置应受变更控制:更具体地说,我认为任何CORS配置的更新都需要仔细审查,并随后重启服务器。[…]如果开发人员认为服务器启动速度过慢,我认为他们应该专注于减少启动时间,而不是避免重启服务器的需求。
使用jub0bs/fcors(我最初的参考实现)构建的中间件实际上是不可变的,任何对相应CORS策略的修改都需要服务器重启。尽管这一约束不能保证CORS策略更改会得到审查,但我相信它至少会推动开发人员采用这种做法。

不过,我小心地给自己留了一些余地:

尽管我努力避免他人过去的设计错误,但我自己很可能犯了全新的错误。

我很高兴这样做了。最近关于jub0bs/cors(我的最新参考实现)的一些反馈迫使我重新思考这一原则,并在这个话题上稍微软化立场。
我仍然非常重视不可变基础设施,但这一约束对于我既定的目标可能既过于限制又无效:

  • 它过于限制,因为它阻碍了快速的零停机更新,至少在某些情况下如此;例如,想象一个多租户SaaS公司的CORS感知API,其(重新)部署时间过长。
  • 它无效,因为正如我在上面引用的段落中暗示的那样,仅凭它不能保证变更控制和可审计性。

回顾过去,我必须承认,试图在库级别解决此类治理问题可能是不明智的。
许多情况需要更务实的方法,只要保持并发安全性,配置不可变性可以有利地取消。

jub0bs/cors中间件现在可重配置

经过一段时间的思考并征求Go社区的一些反馈后,我能够为jub0bs/cors添加运行时中间件重配置功能,这种方式不会破坏我的整个设计理念、引入破坏性更改或损害性能。
jub0bs/cors仍然提供以下函数来创建CORS中间件:

1
func NewMiddleware(cfg Config) (*Middleware, error)

但v0.2.0增加了两个方法:

1
2
func (m *Middleware) Reconfigure(cfg *Config) error
func (m *Middleware) Config() *Config

Reconfigure方法,顾名思义,允许您重新配置中间件m
如果cfg参数非nil但*cfg无效,Reconfigure返回一些非nil错误并保持m不变。
如果cfg为nil,Reconfigure禁用CORS;它基本上将m变成一个“直通”中间件,即一个不做任何有趣事情而仅委托给它包装的处理程序的中间件。
请注意,在Reconfigure返回后修改*cfg的字段不会改变m的行为。

Config方法返回一个指向Config值的深拷贝的指针,该值用于构建或最后重新配置CORS中间件m
借助此方法,您不需要将中间件的配置保留在作用域中以备修改(例如,增加允许的Web来源集);您可以在需要时简单地查询配置。
m.Reconfigure(m.Config())保证是一个无操作(尽管是一个相对昂贵的操作)。
再次请注意,修改Config方法结果的字段不会改变m的行为。

由于这两种方法都是并发安全的,您可以放心地重新配置中间件和/或查询其当前配置,即使它正在并发处理请求。
因此,您可以自由地以某种方式公开这些方法,以便您可以在不重启服务器的情况下执行它们;但是,如果您确实公开这些方法,出于安全原因,您应该仅在某些内部或授权端点上这样做。
此外,重构的一个愉快副产品是Middleware的零值现在可以使用,尽管它仅对应于一个“直通”中间件。

不过,有一点需要注意:请注意频繁的CORS中间件重新配置可能会加剧缓存问题;我参考Jake Archibald关于CORS的博客文章的相关部分。

与rs/cors基于钩子的方法比较

再次,jub0bs/cors可以与rs/cors进行比较,在撰写本文时,它仍然是最流行的Go CORS库。
rs/cors的灵活性源于其多个“钩子”:

1
2
3
4
5
6
7
type Options struct {
  // ...
  AllowOriginFunc            func(origin string) bool
  AllowOriginRequestFunc     func(r *http.Request, origin string) // deprecated
  AllowOriginVaryRequestFunc func(r *http.Request, origin string) (bool, []string)
  // ...
}

通过这些钩子,您可以指定区分允许和不允许的Web来源的策略。
但是,我相信,出于之前文章中解释的原因,这样的钩子容易出错且过于强大,对您自己不利;因此,我坚定地拒绝在我的CORS库中引入类似的东西,并且我仍然这样做。
相反,为了在jub0bs/cors中支持动态中间件重配置,我基本上将rs/cors基于钩子的方法内外翻转,就像翻转袜子一样:
不是指定代表您策略的回调,您必须以*Config值的形式声明性描述您所需的配置,并将该值传递给中间件的Reconfigure方法。
这种方法带来了几个不可低估的好处:

  • 灵活性:您不仅可以更新允许的来源集,还可以更新CORS中间件的整个配置:允许的来源、允许的方法、允许的请求头等。
  • 正确性和安全性:您不能(除错误外)最终得到一个功能失调或不安全的中间件:jub0bs/cors确实拒绝任何它认为无效或不安全的配置,不仅在中间件初始化时,而且在中间件重新配置时。您的中间件要么收到一个干净的更新,要么根本没有更新。
  • 性能:在任何时候,中间件的配置都存在于内存中,在优化用于处理CORS请求的数据结构中;中间件重新配置仍然相对昂贵,尤其是在堆分配方面,但在重新配置频率低于中间件调用(至少一个数量级)的合理假设下,您可以期望良好的整体性能。

jub0bs/cors <= 0.1.2中的关键漏洞

最后,请允许我简短但重要地离题讨论安全。
jub0bs/cors依赖的数据结构之一是一个专门的基数树。
在实现中间件重配置时,我痛苦地意识到,由于我的基数树实现中的一个错误(无疑是由于我变量命名不当引入的),jub0bs/cors包含一个关键漏洞:
在v0.1.2及更早版本中,一些CORS中间件允许一系列不受信任的Web来源。
例如,指定来源模式https://foo.comhttps://bar.com(按此顺序)会产生一个中间件,该中间件会错误地允许不受信任的来源https://barfoo.com
v0.8.0的jub0bs/fcors同样易受攻击。
这个关键漏洞是另一个警示故事,即100%的代码覆盖率不能保证没有错误。
在自己的代码中发现关键漏洞必须是开源开发人员最糟糕的噩梦,但这一事件对我来说特别尴尬:
我不仅自称安全研究人员,而且仅在几天前,我还在抱怨rs/cors的维护者修补一个完全次要漏洞的悠闲步伐,并吹捧jub0bs/cors可能是Go最好的CORS中间件库……😬
Web安全的研究是一门永无止境的谦逊课程。
尽管有最好的意图,一个人编写的每一段软件都必然在某个阶段包含漏洞;重要的是如何响应它们的发现。
在这种情况下,我在4月30日识别并修复了错误;但为了更好的时机,我推迟发布安全公告、通知Go漏洞数据库和发布修补版本(v0.1.3),直到5月2日(CEST)。
我向任何受影响的人道歉,如果您还没有尝试jub0bs/cors,我希望您不会被吓阻。

致谢

感谢Laurent Demailly、Scott Plunkett和Mike Stephen在Gophers Slack、Reddit和私人通信中分别与我分享关于我预期API更改的建设性反馈。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计