本文介绍关于nodejs中的http慢请求攻击问题。
首先我们写一个测试服务器
1
2
3
4
5const http = require('http'); const server = http.createServer((req,res) => {}).listen(80); // 3秒还没有解析完http头则关闭连接 server.headersTimeout = 3000
接着我们写个测试客户端
1
2
3
4
5
6
7
8
9
10
11
12const net = require('net'); const socket = net.connect({port: 80}); socket.write('POST / HTTP/1.1rncontent-length:1rn'); const timer = setInterval(() => { socket.write('1rn') }, 4000); const start = Date.now(); socket.on('end', () => { clearInterval(timer); console.log(Date.now() - start) })
客户端先发送一部分http报文,4秒后再继续发。我们发现,虽然我们服务器设置了3秒超时,但是无效。连接会在4秒后断开。我们看看源码(node14.6.0)为什么。
node_http_parser.cc的OnStreamRead
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 开始解析http的时间,0说明解析完了。如果正在解析且设置了timeout则判断是否超时(server.setTimeout()) if (header_parsing_start_time_ != 0 && headers_timeout_ != 0) { uint64_t now = uv_hrtime(); // 算出已经解析的时间间隔 uint64_t parsing_time = (now - header_parsing_start_time_) / 1e6; // 是否过期了 if (parsing_time > headers_timeout_) { Local<Value> cb = object()->Get(env()->context(), kOnTimeout).ToLocalChecked(); if (!cb->IsFunction()) return; MakeCallback(cb.As<Function>(), 0, nullptr); return; } }
以上是判断解析http头过期的逻辑,看起来没问题,nodejs处理了这个问题,那为什么在我们设定的时间内没有触发断开连接呢?问题在于这段逻辑(OnStreamRead函数)是有数据的时候执行的回调,所以这依赖于客户端发送数据才能触发,否则就不会进入这个逻辑,也就无法断开连接。我们顺便看一下触发这段逻辑的时候会怎样。
1
2
3
4
5
6function onParserTimeout(server, socket) { const serverTimeout = server.emit('timeout', socket); if (!serverTimeout) socket.destroy(); }
默认是断开连接,如果我们监听了server的timeout事件,则需要自己处理连接问题。至此我们明白了为什么服务器的设置无效了。
我们改一下客户端代码再测试一下。
1
2
3
4
5
6
7
8const net = require('net'); const socket = net.connect({port: 80}); socket.write('POST / HTTP/1.1rncontent-length:1rn'); const start = Date.now(); socket.on('end', () => { console.log(Date.now() - start) })
这次我们只发送一部分。超时后也不再发送数据了,这时候服务器会怎样呢?我们发现服务器会一直保持这个连接,经过上面的分析我们知道,当客户端不再发送数据的时候,是无法触发断连的逻辑的,这就会带来http慢请求攻击。那我们如何解决这个问题呢?
nodejs中还有另外一个配置可以帮我们解决这个问题。那就是server.setTimeout();我们看看代码。
1
2
3
4
5
6
7Server.prototype.setTimeout = function setTimeout(msecs, callback) { this.timeout = msecs; if (callback) this.on('timeout', callback); return this; };
设置了一个值。这个值有什么用呢?我们接着看
1
2
3
4
5
6function connectionListenerInternal(server, socket) { if (server.timeout && typeof socket.setTimeout === 'function') socket.setTimeout(server.timeout); socket.on('timeout', socketOnTimeout); }
以上代码是建立tcp连接成功的回调,nodejs会设置socket的空闲超时时间。回调是socketOnTimeout
1
2
3
4
5
6
7
8
9
10function socketOnTimeout() { const req = this.parser && this.parser.incoming; const reqTimeout = req && !req.complete && req.emit('timeout', this); const res = this._httpMessage; const resTimeout = res && res.emit('timeout', this); const serverTimeout = this.server.emit('timeout', this); if (!reqTimeout && !resTimeout && !serverTimeout) this.destroy(); }
超时回调的时候,默认会关闭连接。所以我们可以通过这个配置解决慢请求攻击。
这个问题不是nodejs一直存在的,在早期的版本(比如12.20.0)的时候是没有这个问题的。
1
2
3
4
5
6
7uint64_t http_server_default_timeout = 120000; const kDefaultHttpServerTimeout = getOptionValue('--http-server-default-timeout'); function Server(options, requestListener) { this.timeout = kDefaultHttpServerTimeout; }
我们看到nodejs早期版本里timeout的默认值是两分钟,这意味着nodejs默认处理了这个问题,而v14.6.0中则是0。具体可以参考(https://github.com/nodejs/node/commit/c30ef3cbd2e42ac1d600f6bd78a601a5496b0877 ),这时候我们可能想到keepalive机制,但是linux下keepalive默认是不开启的,而且因为nodejs还没解析完http头,没有回调我们,我们也拿不到socket对象,从而也无法开启keepalive机制。
最后
以上就是独特刺猬最近收集整理的关于nodejs的http慢请求攻击问题的全部内容,更多相关nodejs内容请搜索靠谱客的其他文章。
发表评论 取消回复