Next.js / Node.js 环境中切断这种大文本拒绝服务攻击(DoS)

在 Next.js / Node.js 的请求生命周期中,中间件就像是守卫在城堡大门口的哨兵。当一个请求从客户端发出,它会先经过中间件,然后才会到达具体的路由处理器(Route Handlers)或服务器组件(Server Components)。

如果等到请求进入了业务代码再去限制体积,Node.js 此时可能已经把这个大文本读入内存了,这无法从根本上防御 DoS 攻击。

在中间件这一层,我们可以利用 HTTP 请求头中的 Content-Length 来做文章。这个请求头记录了整个请求体的字节大小(Byte)。我们可以直接在这里设置一条"硬红线"。

通常,一个正常的导航菜单配置 JSON 数据不会超过几百 KB。如果我们把上限设为 1MB 或者是 2MB,就能挡掉绝大多数恶意的大文件轰炸。

让我们来看看在 Next.js 的 middleware.ts 中可以如何实现这个拦截逻辑:

import 复制代码
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // 1. 🔍 只针对修改菜单的 POST / PUT 请求进行体积检查
  if (request.nextUrl.pathname.startsWith('/api/admin/menu')) {
    
    // 2. 📏 获取请求头中的数据体积大小 (单位: 字节)
    const contentLength = request.headers.get('content-length');
    
    if (contentLength) {
      const sizeInBytes = parseInt(contentLength, 10);
      const ONE_MEGABYTE = 1024 * 1024; // 1MB
      
      // 3. 🚨 超过红线,直接弹回,不给它进入服务器解析的机会
      if (sizeInBytes > ONE_MEGABYTE) {
        return new NextResponse(
          JSON.stringify({ error: '请求体体积过大,拒绝服务' }),
          { status: 413, headers: { 'content-type': 'application/json' } }
        );
      }
    }
  }

  return NextResponse.next();
}

黑客也可能会非常狡猾。如果黑客故意伪造了 HTTP 请求头,把 Content-Length 故意写得很小(比如写了 100 字节),但实际上底层却通过 HTTP 分块传输(Chunked Transfer Encoding)源源不断地发送了几百兆的数据流,你觉得我们的中间件还能防住它吗?我们还需要配合什么机制来彻底锁死它呢 黑客确实可以通过分块传输来伪造 Content-Length,哨兵在这个时候就会被欺骗。只检查头部是不够的,我们必须对请求体(Request Body)的实际流入量进行实时监控。

在 Node.js 和 Next.js 的路由处理器(Route Handlers)中,标准的做法是流式读取(Streaming)请求体,并在读取过程中计算已接收的字节数。一旦实际读取的字节数超过阈值,立刻掐断连接。

我们可以使用原生的 TransformStream 或者结合 safe-json-parse 来实现这一层防御。

🛡️ 3. 实战:限制实际流入的 Body 体积

在我们的路由处理器中,我们可以写一个通用的解析助手函数:

import 复制代码
import { scan } from 'secure-json-parse';

// 限制实际读取的最大体积为 1MB
const MAX_ALLOWED_SIZE = 1024 * 1024; 

async function parseAndValidateSafeBody(request: NextRequest) {
  const reader = request.body?.getReader();
  if (!reader) throw new Error('请求体为空');

  let totalBytes = 0;
  const chunks: Uint8Array[] = [];

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    totalBytes += value.length;
    
    // 🚨 核心防御:在读取流的过程中,一旦发现实际体积超标,立刻熔断!
    if (totalBytes > MAX_ALLOWED_SIZE) {
      throw new Error('413_PAYLOAD_TOO_LARGE');
    }

    chunks.push(value);
  }

  // 将所有数据块合并并转为字符串
  const concatBuffer = new Uint8Array(totalBytes);
  let offset = 0;
  for (const chunk of chunks) {
    concatBuffer.set(chunk, offset);
    offset += chunk.length;
  }
  
  const rawText = new TextDecoder().decode(concatBuffer);

  // 🔒 结合我们第一维学的 secure-json-parse 杜绝原型链污染
  return scan(rawText, { protoAction: 'error', constructorAction: 'error' });
}
相关推荐
二喵❥(^_-)2 小时前
Node下载和安装
node.js
wgc2k2 小时前
Node.js游戏服务器项目移植-1:项目移植版本环境的冲突问题以及技术选型测试
游戏·node.js
凌览2 小时前
为什么我不推荐一人公司用PostgreSQL
前端·后端·node.js
wgc2k5 小时前
Node.js游戏服务器项目移植-2: 用TypeScript还是Javascript
服务器·游戏·node.js
卷帘依旧17 小时前
v8引擎和libuv的关系
node.js
wuxia211821 小时前
用Node.js为网站首页绑定数据
javascript·node.js
cmdyu_21 小时前
mac上如何卸载node.js
macos·node.js
大家的林语冰1 天前
Express 团队官宣:全新网站正式上线,Logo 重做,支持两个主版本文档无缝切换!
javascript·node.js·express
右耳朵猫AI1 天前
Node.js技术周刊 2026年第20周
node.js