Node.js 独立产品的四个安全漏洞:从注入攻击到权限越界

📅 2026/7/27 11:33:07
Node.js 独立产品的四个安全漏洞:从注入攻击到权限越界
Node.js 独立产品的四个安全漏洞从注入攻击到权限越界一、独立产品的安全盲区独立产品的安全防护往往处于一种尴尬状态既没有大公司的安全团队做渗透测试也没有足够的攻击面吸引白帽子提交漏洞。结果是很多独立产品上线两年从未被专业安全审计但攻击者不一定在乎你多有名——自动化漏洞扫描工具对所有暴露在公网的服务一视同仁。以下是在审计 5 个 Node.js 独立产品的代码时发现的最常见的四类安全漏洞。这些都是一眼就能被发现但开发者从没意识到的问题。二、漏洞一NoSQL 注入 —— MongoDB 不是 SQL但仍有注入风险很多开发者认为MongoDB 不需要防注入因为它不是关系型数据库。这是对注入攻击的最大误解。MongoDB 的$where、$regex、以及未经验证的查询参数拼接与 SQL 注入一样危险。// 危险的 MongoDB 查询 —— NoSQL 注入 // Express 路由POST /api/users/login app.post(/api/users/login, async (req, res) { const { username, password } req.body; // 攻击者发送 // { username: { $gt: }, password: { $ne: } } // MongoDB 查询变成 // db.users.findOne({ username: { $gt: }, password: { $ne: } }) // 结果匹配到任意用户绕过密码验证 const user await db.collection(users).findOne({ username, password, // 明文密码是另一个安全问题 }); if (user) { // 攻击者成功登录 res.json({ token: generateToken(user) }); } });修复方案import { sanitize } from express-mongo-sanitize; // 方案 1使用 express-mongo-sanitize 中间件 app.use(sanitize({ // 移除所有以 $ 或 . 开头的 key replaceWith: _, })); // 方案 2手动验证输入值类型 function validateLoginInput(body: unknown): { username: string; password: string; } { if (typeof body ! object || body null) { throw new Error(Invalid request body); } const { username, password } body as Recordstring, unknown; // 确保 username 和 password 是字符串不是对象 if (typeof username ! string || typeof password ! string) { throw new Error(username and password must be strings); } // 长度限制 if (username.length 100 || password.length 128) { throw new Error(Input too long); } return { username, password }; } // 方案 3使用 Mongoose 的 schema validation const UserSchema new mongoose.Schema({ username: { type: String, required: true, maxlength: 100 }, password: { type: String, required: true, select: false }, }); // Mongoose 自动将 { $gt: } 转换为字符串不会被解释为查询操作符额外提醒即使是关系型数据库ORM如 Prisma、TypeORM也不能完全防止注入。原生查询和动态排序字段仍然是高风险点// ORM 也无法防止的注入场景 async function getUsers(orderBy: string, direction: string) { // 攻击者orderBy 1; DROP TABLE users; -- return prisma.$queryRawUnsafe( SELECT * FROM users ORDER BY ${orderBy} ${direction} ); } // 正确做法白名单校验 const ALLOWED_ORDER_FIELDS [id, username, createdAt, email]; const ALLOWED_DIRECTIONS [ASC, DESC]; function getUsers(orderBy: string, direction: string) { if (!ALLOWED_ORDER_FIELDS.includes(orderBy)) { throw new Error(Invalid order field); } if (!ALLOWED_DIRECTIONS.includes(direction.toUpperCase())) { throw new Error(Invalid direction); } return prisma.$queryRawUnsafe( SELECT * FROM users ORDER BY ${orderBy} ${direction.toUpperCase()} ); }三、漏洞二JWT 认证的隐蔽坑JWT 在独立产品中被广泛使用但大多数实现存在以下问题之一问题 1不验证签名算法// 危险不限制算法攻击者可以将算法改为 none const decoded jwt.verify(token, SECRET_KEY); // 缺少 { algorithms: [HS256] } 参数 // 攻击者可以伪造 token // header: { alg: none, typ: JWT } // payload: { userId: admin, role: admin } // signature: 问题 2使用弱密钥// 常见的弱密钥 const SECRET_KEY mysecretkey; // 太短可暴力破解 const SECRET_KEY your-256-bit-secret; // 从网上 copy 的示例密钥 // 生成安全密钥 // node -e console.log(require(crypto).randomBytes(64).toString(hex)) const SECRET_KEY process.env.JWT_SECRET; // 使用环境变量至少 256 位问题 3Token 无过期时间或过期时间过长// 危险的 JWT 签发 const token jwt.sign( { userId: user.id }, SECRET_KEY // 缺少 expiresIn // Token 永不过期一旦泄露无法撤销 ); // 正确的 JWT 签发 const ACCESS_TOKEN_EXPIRY 15m; // 短时效 const REFRESH_TOKEN_EXPIRY 7d; // 长时效但可撤销 function generateTokens(user: User) { const accessToken jwt.sign( { userId: user.id, type: access }, ACCESS_TOKEN_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY, algorithm: HS256 } ); const refreshToken jwt.sign( { userId: user.id, type: refresh, tokenId: crypto.randomUUID() }, REFRESH_TOKEN_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY, algorithm: HS256 } ); // 将 refresh token 的 tokenId 存入数据库支持撤销 await db.refreshTokens.create({ tokenId: extractTokenId(refreshToken), userId: user.id, expiresAt: new Date(Date.now() 7 * 24 * 60 * 60 * 1000), }); return { accessToken, refreshToken }; } // 验证时限制算法 function verifyAccessToken(token: string): JwtPayload { return jwt.verify(token, ACCESS_TOKEN_SECRET, { algorithms: [HS256], // 只接受 HS256 }) as JwtPayload; }问题 4将敏感信息放在 JWT payload 中// 危险JWT payload 是 base64 编码不是加密 // 任何人都可以解码查看其中的内容 const token jwt.sign({ userId: user.id, email: user.email, // 敏感 phoneNumber: user.phone, // 敏感 creditCard: user.cardLast4, // 极其敏感 }, SECRET_KEY); // JWT payload 只应包含 // 1. 用户标识userId // 2. Token 类型access/refresh // 3. 过期时间exp由库自动添加 // 4. 签发时间iat由库自动添加 // 不要在 JWT 中放敏感信息四、漏洞三依赖供应链攻击Node.js 项目的node_modules是最大的安全攻击面。一个项目平均有 800 到 1200 个直接和间接依赖。任何一个依赖的任何一个版本被投毒整个产品就可能被攻破。真实案例2022 年colors.js和faker.js的作者故意在包中引入恶意代码无限循环影响了数千个项目。2024 年xz-utils后门事件攻击者潜伏两年获取了维护权限并植入了 SSH 后门。event-stream事件攻击者通过获取一个流行 npm 包每周 200 万下载量的维护权限植入了窃取比特币钱包的代码。典型攻击路径攻击者先找到一个无人维护但仍有大量下载量的 npm 包接管其维护权限通过提交 PR 表示愿意维护发布一个看似正常的更新版本在postinstall脚本中植入恶意代码。这个脚本在npm install时自动执行窃取环境变量中的 API Key 或向外部服务器发送用户数据。大多数开发者执行npm install时不会检查每个包的postinstall脚本内容。// 供应链安全的多层防护 // 1. 锁定依赖版本使用 lockfile // package-lock.json 或 yarn.lock 必须提交到仓库 // 不要使用 ^ 或 ~ 版本范围在生产环境 // 2. 定期审计依赖 // npm audit // npm audit fix // 将 npm audit 集成到 CI 中 // package.json { scripts: { audit: npm audit --audit-levelhigh, ci: npm run audit npm run build } } // 3. 使用 Snyk / Socket.dev 等专业工具做深度扫描 // 检查项目依赖中是否存在 // - 已知 CVE 漏洞 // - 安装后脚本可能有恶意代码 // - 网络请求、文件系统访问 // - 使用了 eval、child_process 等危险 API最小化攻击面的策略# 定期清理未使用的依赖 npx depcheck # 检查 node_modules 体积 du -sh node_modules # 列出所有依赖及相关信息 npx npm-remote-ls package-name # 使用 pnpm 代替 npm严格的依赖隔离幽灵依赖问题更少 pnpm install --frozen-lockfile # 在 CI 中锁定安装行为 npm ci # 严格按 lockfile 安装不更新版本依赖引入的审批流程// 每次新增依赖前开发者需要回答 interface DependencyReview { packageName: string; // 是否真的需要能否不引入依赖实现 isNecessary: boolean; // 每周下载量 10万太小的包风险更高 weeklyDownloads: number; // 最近一次发布时间超过 6 个月未更新需警惕 lastPublished: Date; // 维护者数量 1单人维护的包风险更高 maintainerCount: number; // 是否有 install/postinstall 脚本 hasInstallScripts: boolean; // 依赖的传递依赖数量 transitiveDependencyCount: number; // 打包体积增量 bundleSizeIncrease: number; } // 审批规则 // - weeklyDownloads 1000 且 maintainerCount 1 → 拒绝 // - hasInstallScripts true → 需要额外安全审查 // - transitiveDependencyCount 50 → 评估是否必要五、漏洞四权限越界 —— 接口层缺少身份验证独立产品最常见的权限漏洞是相信前端做了权限校验。前端通过路由守卫隐藏了某些页面但后端的 API 接口没有任何身份验证。// 漏洞场景管理员接口没有任何身份验证 // GET /api/admin/users —— 返回所有用户信息 app.get(/api/admin/users, async (req, res) { // 没有验证当前用户是否是管理员 const users await db.users.find().toArray(); res.json(users); }); // 攻击者直接用 curl 调用即可获取全量用户数据 // curl https://api.example.com/api/admin/users修复分层身份验证中间件// 身份验证中间件 function authenticate(req: Request, res: Response, next: NextFunction) { const token req.headers.authorization?.replace(Bearer , ); if (!token) { return res.status(401).json({ error: Unauthorized }); } try { const payload jwt.verify(token, JWT_SECRET, { algorithms: [HS256] }); req.user payload as UserPayload; next(); } catch { return res.status(401).json({ error: Invalid token }); } } // 角色验证中间件 function authorize(...roles: string[]) { return (req: Request, res: Response, next: NextFunction) { if (!req.user) { return res.status(401).json({ error: Unauthorized }); } if (!roles.includes(req.user.role)) { return res.status(403).json({ error: Forbidden }); } next(); }; } // 资源归属验证中间件 async function ownsResource( req: Request, res: Response, next: NextFunction ) { const resourceId req.params.id; const resource await db.resources.findById(resourceId); if (!resource) { return res.status(404).json({ error: Not found }); } // 管理员可以看任何资源 if (req.user.role admin) { req.resource resource; return next(); } // 普通用户只能看自己的资源 if (resource.userId ! req.user.userId) { return res.status(403).json({ error: Forbidden }); } req.resource resource; next(); } // 实际使用 app.get( /api/admin/users, authenticate, // 先验证登录 authorize(admin), // 再验证角色 getAdminUsers ); app.get( /api/orders/:id, authenticate, ownsResource, // 验证资源归属 getOrderDetail );常见的越权漏洞模式// 漏洞 1IDORInsecure Direct Object Reference // GET /api/invoices/123 —— 用户可以看发票 123 // 如果用户修改 URL 为 /api/invoices/456能看到别人的发票 // 根本原因没有验证发票 456 是否属于当前用户 // 漏洞 2批量操作无权限校验 // POST /api/users/batch-delete // Body: { userIds: [1, 2, 3] } // 普通用户不应有删除用户的权限但接口没有校验 // 漏洞 3请求方法篡改 // 前端只渲染了 GET 请求的表单攻击者用 POST 发送同样的数据 // PUT /api/projects/123 —— 前端没有修改按钮但接口存在六、安全加固自检清单// 生产环境安全检查清单 const SECURITY_CHECKLIST { // 输入验证 inputValidation: { sanitizeMongoDB: true, // express-mongo-sanitize validateContentType: true, // 只接受 JSON rateLimiting: true, // express-rate-limit bodySizeLimit: 10kb, // 限制请求体大小 }, // 认证授权 authentication: { jwtAlgorithmRestricted: true, // 只允许 HS256 tokenExpiry: 1h, // access token 过期时间 refreshTokenRevokable: true, // refresh token 可撤销 passwordHashed: true, // bcrypt / argon2 minPasswordLength: 8, }, // 安全头 securityHeaders: { helmet: true, // helmet 中间件 csp: true, // Content-Security-Policy hsts: true, // Strict-Transport-Security }, // 依赖安全 dependencySecurity: { lockfileCommitted: true, // lockfile 在仓库中 automatedAudit: true, // CI 中自动 npm audit knownVulnerabilitiesFixed: true, minDependencyReview: true, // 新增依赖必经审查 }, // 数据保护 dataProtection: { sensitiveDataEncrypted: true, // PII 数据加密存储 loggingNoSensitiveData: true, // 日志不含敏感信息 envVarsNotInCode: true, // .env 不在仓库中 }, }; // 自动化安全检查 import helmet from helmet; import rateLimit from express-rate-limit; import mongoSanitize from express-mongo-sanitize; app.use(helmet()); // 安全头 app.use(mongoSanitize()); // NoSQL 注入防护 app.use(express.json({ limit: 10kb })); // 限制请求体大小 app.use(/api/, rateLimit({ windowMs: 15 * 60 * 1000, // 15 分钟窗口 max: 100, // 每个 IP 最多 100 次请求 message: { error: Too many requests }, })); // 敏感路径更严格的限流 app.use(/api/auth/, rateLimit({ windowMs: 15 * 60 * 1000, max: 5, // 登录接口 15 分钟最多 5 次 }));五、总结Node.js 独立产品安全防护的核心要点NoSQL 注入与 SQL 注入同样危险MongoDB 的$where、$regex和未验证的查询参数拼接可绕过认证必须使用express-mongo-sanitize 类型校验双保险。JWT 认证的四个隐蔽坑限制算法为 HS256、设置过期时间、使用 256 位以上强密钥、payload 不放敏感信息——任一项遗漏都可能被攻击者利用。供应链是最大攻击面800 间接依赖中任何一个被投毒就危及全系统npm audit集成 CI 新增依赖审批流程是最小投入最大回报的安全建设。API 层权限校验不可省略不依赖前端隐藏按钮的假设分层中间件认证 → 授权 → 资源归属是防止 IDOR 和越权操作的唯一可靠方案。可执行建议本周完成三项安全加固——安装 helmet express-mongo-sanitize express-rate-limit 中间件、JWT 签发时加上algorithms: [HS256]expiresIn: 15m、CI 中加入npm audit --audit-levelhigh。三项改动一天可完成覆盖 80% 的常见攻击面。七、总结Node.js 独立产品的四个核心安全漏洞及解决方案漏洞类型常见表现修复方案NoSQL 注入直接将请求体传进查询未验证类型express-mongo-sanitize 输入类型校验JWT 认证缺陷弱密钥、无过期、未限制算法强密钥 expiresInalgorithms: [HS256]供应链攻击依赖未审计盲目引入npm audit 依赖审批流程 最小化攻击面权限越界API 层无身份和资源归属验证分层中间件认证 → 授权 → 资源归属三个最重要的安全投入按优先级输入验证和参数化查询修复注入类漏洞这是最有性价比的安全投入。依赖审计自动化将npm audit集成到 CI 中定期扫描阻止高危依赖引入。API 层权限校验在路由层统一应用身份验证和授权中间件不使用依赖前端不显示就不会被调用的假设。独立产品的安全不是一次性的工作而是需要在每次代码提交、每次依赖更新、每次部署时持续关注的基础设施。