nodejs的http慢请求攻击问题
发布于 1 个月前 作者 theanarkh 826 次浏览 来自 分享

本文介绍关于nodejs中的http慢请求攻击问题。 首先我们写一个测试服务器

const http = require('http');
const server = http.createServer((req,res) => {}).listen(80);
// 3秒还没有解析完http头则关闭连接
server.headersTimeout = 3000

接着我们写个测试客户端

const net = require('net');
const socket = net.connect({port: 80});
socket.write('POST / HTTP/1.1\r\ncontent-length:1\r\n');
const timer = setInterval(() => {
    socket.write('1\r\n')
}, 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

// 开始解析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函数)是有数据的时候执行的回调,所以这依赖于客户端发送数据才能触发,否则就不会进入这个逻辑,也就无法断开连接。我们顺便看一下触发这段逻辑的时候会怎样。

function onParserTimeout(server, socket) {
  const serverTimeout = server.emit('timeout', socket);

  if (!serverTimeout)
    socket.destroy();
}

默认是断开连接,如果我们监听了server的timeout事件,则需要自己处理连接问题。至此我们明白了为什么服务器的设置无效了。 我们改一下客户端代码再测试一下。

const net = require('net');
const socket = net.connect({port: 80});
socket.write('POST / HTTP/1.1\r\ncontent-length:1\r\n');
const start = Date.now();
socket.on('end', () => {
    console.log(Date.now() - start)
})

这次我们只发送一部分。超时后也不再发送数据了,这时候服务器会怎样呢?我们发现服务器会一直保持这个连接,经过上面的分析我们知道,当客户端不再发送数据的时候,是无法触发断连的逻辑的,这就会带来http慢请求攻击。那我们如何解决这个问题呢? nodejs中还有另外一个配置可以帮我们解决这个问题。那就是server.setTimeout();我们看看代码。

Server.prototype.setTimeout = function setTimeout(msecs, callback) {
  this.timeout = msecs;
  if (callback)
    this.on('timeout', callback);
  return this;
};

设置了一个值。这个值有什么用呢?我们接着看

function connectionListenerInternal(server, socket) {
  if (server.timeout && typeof socket.setTimeout === 'function')
    socket.setTimeout(server.timeout);
  socket.on('timeout', socketOnTimeout);
}

以上代码是建立tcp连接成功的回调,nodejs会设置socket的空闲超时时间。回调是socketOnTimeout

function 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)的时候是没有这个问题的。

 uint64_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机制。

回到顶部