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' });
}
相关推荐
用户8471810541920 小时前
从0开始打造自己的个人智能体(一)——Deep Agents js环境配置与对话入门
node.js·agent
减瓦1 天前
从 0.1 到 26:一个运行时的十七年进化史
node.js
晓得迷路了1 天前
栗子前端技术周刊第 140 期 - pnpm、Node 新 API 文档网站、Oxlint...
前端·javascript·node.js
孟陬2 天前
Node.js 测试报告格式一览和自定义 json 格式 `--test-reporter=spec | tap | dot | junit | lcov`
node.js
陳陈陳3 天前
🚀 前端流式输出革命:SSE + BFF 架构从零到一实战指南
vue.js·架构·node.js
Revolution613 天前
第一次运行 Node.js:终端里的 JavaScript 怎样执行
后端·面试·node.js
半句唐诗3 天前
我是如何通过 Access Token 成功发布第一个 npm 包的
前端·npm·node.js
贩卖黄昏的熊3 天前
NestJS简明教程——异常处理和日志
javascript·node.js·nest.js
Revolution613 天前
Node.js 是什么:前端项目里哪些事情由它完成
前端·面试·node.js
Revolution613 天前
Nest.js 是什么:怎样用它写出第一个后端接口
后端·node.js·nestjs