Niggurath 是一个基于 CloudFlare Worker 的伪无状态 PoW 认证服务概念验证项目。它通过要求客户端在登录前解决计算难题来防止暴力破解攻击。
CloudFlare Worker在Client-TCP-Server没有断开之前不会重置Worker实例的内存状态,因此可以通过在Worker内存中存储部分状态来实现伪无状态的PoW认证服务。
尽管HTTP被设计为无状态的,但是TCP不是。自从HTTP/1.0引入了持久连接(Persistent Connection)以来,客户端和服务器之间的TCP连接可以在多个HTTP请求/响应周期内保持打开状态。尤其地,在HTTP/1.1中,持久连接是默认启用的,而在这之后HTTP/2更是强制了持久连接。
要命的是这个TCP连接复用还是系统控制的。换句话说你在浏览器里打开一个网页,然后本地代开curl去请求同一个服务器,浏览器和curl发起的请求很可能会复用同一个TCP连接。
CloudFlare Worker尽管也是设计为无状态的(Worker处理HTTP请求理论上和之前的HTTP请求应当无关)。但是,由于不知名原因1,CloudFlare Worker的生命周期实际上由TCP控制。在整个TCP存活期间,Worker实例的内存状态依旧还保留着。直到TCP连接关闭,内存状态才会被重置。
尽管HTTP/3使用QUIC(基于UDP)本身也是无状态的。但是根据实际测试而言,Niggurath仍然在HTTP/3下工作正常!2
因此,无状态被打破。通过这种方式,Worker可以在内存中保留状态。
在这里可以给出一个简单的例子:
let n = 0
addEventListener('fetch', e => e.respondWith(new Response(++n)))访问这个Worker,你会发现数字一直在增长 -- 直到TCP连接关闭。我们可以称这种情况叫状态被临时保留。
一直以来,如何在一堆流量中区分人与机器一直是一个难题。
但是在某种程度上,我们需要区分的不一定是人和机器,而是恶意流量和正常流量。
例如,对于一个登录接口,很多情况下网站管理员并不关心按下登录按钮的是人还是机器。他们只关心的是:这个请求是不是恶意的?,或者换句话说,这个请求是不是暴力破解攻击的一部分?
只不过暴力破解攻击通常是由机器发起的罢了。
什么,你在说廉价劳动力?
因此对于传统的验证,更关心的是这个用户的恶意值,最经典的例子就是短时间内对同一个接口发起大量请求。
某种意义上来说,让每个请求Delay一段时间,或者给用户限速,就能避免单一IP暴力破解攻击。
但是话又说回来,如果我整一个代理池子,然后每个请求都用不同的IP地址发起请求,阁下又能怎么区分每个请求的目的?
某些脑子不正常的服务端会直接禁用待登陆的用户名,这里点名批评微软远程桌面,默认情况下一个用户登录10次给用户禁用了真的有点智熄了。
到这里不妨换个思路,让每个用户在请求时都要付出一定的计算代价,这样一来,无论是正常用户还是恶意用户在请求一次时都要付出少量的计算代价,在客观上提高了用户两次合法请求之间的时间间隔。
而对于暴力破解攻击来说,由于攻击者需要发起大量请求,因此计算代价的累积会让攻击变得非常昂贵,从而在经济上阻止暴力破解攻击。
此处的计算代价可以通过工作量证明(Proof of Work, PoW)来实现。这里的灵感也来自于Anubis,这是一个能够通过PoW来实现合法用户验证的开源项目。
Anubis很好,但在架构上不适应纯无状态的服务。(尤其地对于纯Worker应用而言)
- Anubis工作在Middleware层,通过反向代理后端来实现认证。
- Anubis是设计在有状态的服务上的,通过内存、磁盘或Valkey存储认证状态。
如果用真正意义上的无状态认证服务,那么就会出现一个问题:认证的密钥不能被主动失效。
换句话说,如果我正常获得了一个认证密钥,那么我就可以无限制地使用它。那么无论在这之前我用了多么严格的认证服务(包括邮箱、SMS或者图形验证码),只要通过一次拿到认证密钥,这些“绊脚石”都是形同虚设。
所以大多数情况下,无状态认证密钥被设计为有时间限制的。但是这个取时间又很难把握:时间太短,网络情况差一点就认证不上了。时间太长,嚯嚯嚯骇客直接开炫。而且无论设计的有多短,都无法避免重放攻击。
那我们先前提及的CloudFlare Worker的伪无状态特性就作为最后一块拼图补上了这个漏洞。认证一次,Challenge即刻失效。
换句话说,只要确保用户GetChallenge和实际请求在同一个TCP连接内,就可以实现无状态PoW认证。而这,就是Niggurath的设计思路。
但其实某种意义上来说,这更像是有状态认证服务的内存变种罢了。只不过这个状态保存在无状态HTTP上显得很诡异,因此我更愿意称之为伪无状态。
准确的说这个仓库只是一个概念验证(Proof of Concept, PoC),并不适合直接用于生产环境。
开坨屎山来给大伙评鉴说是
而且Niggurath目前的只是给我自己的EurekaCAuth服务做地PoW认证的前置防护,目前也没有很好的思路去作为一个包并入实际使用中。
不过如果你想玩玩看,点击下方按钮一键Deploy。
因为用TCP来维持HTTP的方式本身就是Feature,所以在一些非常奇怪/极差的网络环境下,Niggurath (KeepAlive)可能无法正常工作。
此外,在通过其他CDN/中间盒/中间件Middleware的情况下,由于中继节点采取的策略不同,并不一定遵守HTTP持久连接的规范,因此Niggurath也可能无法正常工作。
此时Niggurath允许使用WebSocket作为Fallback连接方式(待实现)。
但是,我们通常建议将待验证的接口(例如登录、注册接口)直接进行Niggurath验证。使用WebSocket可能会使实际逻辑编写变得复杂。
不过,大部分测试是有效的:
- Internet Explorer 11 (需要Polyfill,
IE也来啦) - Tor
- Tencent Cloud CDN (EdgeOne)
- Another CloudFlare Worker
- PHP Reverse Proxy
- Nginx Reverse Proxy
以下是CyanFalse实际测试结果:
| CPU | 环境 | 难度(即计算长度) | 最差运气平均耗时 | 速度 | 备注 |
|---|---|---|---|---|---|
| x86 Intel i5-12600 | Chrome 142 | 5 | 1080ms | 92it/ms | 使用WebWorker |
| x86 Intel i5-12600 | Chrome 142 | 5 | 1266ms | 78.9it/ms | 不使用WebWorker |
| x86 Intel i5-12600 | Tor 15,FireFox 140 | 5 | 2455ms | 40.8it/ms | 使用WebWorker |
| ARM Snapdragon 8+ Gen 1 | Webview Chrome 103 | 5 | 10315ms | 9.7it/ms | 使用WebWorker |
| ARM Snapdragon 8+ Gen 1 | Webview Chrome 144 | 5 | 2820ms | 35.5it/ms | 使用WebWorker |
| ARM Neoverse N1 | Webview Chrome 142 | 5 | 4033ms | 24.8it/ms | 使用WebWorker |
这在现代设备上完全是一个轻量级任务。但需要注意,较旧的浏览器内核和不支持WebWorker的环境可能会导致性能显著下降。并且,当不使用WebWorker时,由于主线程阻塞,用户体验可能会受到影响(表现为页面冻结)。如果使用settimeout等方法将计算任务踢出主线程,将会导致计算效率急剧下降到2-3it/ms。
尽管Niggurath提供了onProgress回调来反馈计算进度,并且Niggurath推荐在用户界面上展示计算进度,但CyanFalse仍不建议将计算难度(即长度)设置为5以上,除非你通过其他方式认为本请求极具风险,需要迫使用户进行更高强度的计算。
- PoW认证服务概念验证
- 时间有效的Challenge令牌
- 使用Web Worker加速Challenge计算
- 使用WebSocket作为Fallback连接方式
-
通过劫持fetch来允许Niggurath作为第三方中间件使用无计划,提供基础API。
GPL-3.0 License
