对于那些刚开始接触安全领域的开发者来说,有这样一个现象往往会让他们感到惊讶:大多数Web漏洞其实并非由复杂的攻击行为所导致。这些漏洞往往源于一些看似完全合理的代码模式——比如信任URL中传递的参数值、将请求体直接用于数据库更新操作,或者明明只需要执行一次查询却实际执行了两次。

本指南涵盖了其中六种常见的代码模式。对于每种模式,我们都会提供一个具体的代码示例来说明这种模式是如何导致漏洞产生的,同时解释其危险性,并提供修改后的代码版本,同时详细说明哪些地方发生了变化以及为什么需要做出这些修改。

阅读本指南并不要求读者具备任何安全方面的专业知识,不过熟悉Node.js和SQL会有一定的帮助。

先决条件

这些示例假设您已经掌握了以下内容:

  • Node.js和Express.js的基础知识

  • SQL查询语句的编写方法

  • HTTP请求与响应的工作原理

  • 基本的身份验证机制(会话、令牌等)

关于代码示例的说明:在本教程中,db.query()是一个虚构的数据库操作函数。对于SELECT查询,它会返回一个包含查询结果的单行对象;如果未找到匹配记录,则会返回null;而对于INSERT/UPDATE查询,它则会返回一个包含所有修改结果的对象。在讨论“竞态条件”这一主题时使用的connection.query()函数直接调用了mysql2库中的Promise API,此时query()会返回一个包含rowsfields的数组。请根据您实际使用的数据库驱动程序调整相关语法。

目录

1. 访问控制漏洞与IDOR攻击

自2021年以来,“访问控制漏洞”一直位列OWASP十大安全威胁之首,其原因显而易见。其中最常见的漏洞类型就是不安全的直接对象引用(IDOR)攻击:应用程序会在URL中暴露数据库记录的ID,如果用户擅自修改了这个ID,就会非法访问其他用户的资料。

事后看来,这种漏洞的解决方法似乎非常简单。但实际上,在许多生产环境中,这类问题仍然屡见不鲜,因为身份验证和权限控制这两个环节经常被混淆。确认用户已经登录与确认他们是否有权访问特定资源是两回事。

如何在自己的代码中识别这种漏洞

以下是一个典型的用户资料端点示例:

// Express.js – 存在安全漏洞
app.get('/api/users/:id/profile', authenticate, async (req, res) => {
  const userId = req.params.id;

  const user = await db.query(
    'SELECT id, name, email, address FROM users WHERE id = ?,'
    [userId]
  );

  if (!user) {
    return res.status(404).json({ error: '用户未找到' });
  }

  res.json(user);
});

authenticate中间件会验证请求中是否包含有效的令牌,但并不会检查经过身份验证的用户是否被允许访问所请求的资源。

任何经过身份验证的用户都可以请求/api/users/1/profile/api/users/2/profile等路径,从而获取其他用户的资料。

为什么这很重要

身份验证用于确认“你是谁”,而授权则用于确定“你被允许做什么”。上述代码只完成了身份验证的部分,完全忽略了授权环节。

如果使用顺序编号作为用户标识符,好奇的用户无需任何特殊工具,只需将URL中的1改为2即可获取其他用户的资料。但如果使用UUID或简写地址格式,且没有进行所有权验证,同样的问题依然存在。

如何修复这一安全漏洞

在服务器端,必须确认经过身份验证的用户确实拥有或被明确授权访问所请求的资源:

// Express.js – 加强安全性
app.get('/api/users/:id/profile', authenticate, async (req, res) => {
  // 如果参数不是数字字符串,就会抛出错误
  if (!/^\d+$/.test(req.params.id)) {
    return res.status(400).json({ error: '用户ID无效' });
  }

  const requestedId = Number(req.params.id);

  // 如果用户ID小于1,说明输入有误
  if (requestedId < 1) {
    return res.status(400).json({ error: '用户ID无效' });
  }

  // 经过身份验证的用户的ID由authenticate中间件设置
  const authenticatedId = req.user.id;

  // 确保用户只能访问自己的资料
  if (requestedId !== authenticatedId) {
    return res.status(403).json({ error: '禁止访问' });
  }

  const user = await db.query(
    'SELECT id, name, email, address FROM users WHERE id = ?,'
    [requestedId]
  );

  if (!user) {
    return res.status(404).json({ error: '用户未找到' });
  }

  res.json(user);
});

对于那些确实需要访问任何用户信息的管理员端点,必须明确实施基于角色的授权机制:

// 包含明确角色验证的管理员端点
app.get('/api/admin/users/:id', authenticate, requireRole('admin'), async (req, res) => {
  if (!/^\d+$/.test(req.params.id)) {
    return res.status(400).json({ error: '用户ID无效' });
  }

  const userId = Number(req.params.id);

  const user = await db.query(
    'SELECT id, name, email, role FROM users WHERE id = ?,'
    [userId]
  );

  if (!user) {
    return res.status(404).json({ error: '用户未找到' });
  }

  res.json(user);
});

以下是发生变化的内容及其原因:

  • /^\d+$/.test(req.params.id)会拒绝任何非纯数字字符串。例如,parseInt("12abc", 10)会默默地将“12abc”转换为数字12,并让其通过后续检查。而正则表达式能够有效防止这种情况发生。

  • Number(req.params.id)会安全地将已经经过验证的字符串转换为数字。

  • 比较语句requestedId !== authenticatedId用于确保操作权限的正确性。

  • 管理员功能是通过独立的接口提供的,该接口也有自己的授权检查机制。

切勿仅根据URL参数来判断用户的授权级别,而应通过已认证的会话信息来确定。

2. 批量赋值功能

批量赋值是一种在编写代码时几乎难以被察觉的安全漏洞。你只是想提高效率而已,对吧?既然可以直接传递整个对象,为什么还要手动遍历每个字段呢?

问题在于,你的数据库表中包含了用户本不应能够修改的字段。

如何在代码中识别这种漏洞

以下是一个用于更新用户信息的接口示例:

// Express.js 示例——存在安全漏洞
app.put('/api/users/me', authenticate, async (req, res) => {
  const userId = req.user.id;

  // req.body中包含了客户端发送的所有数据
  const updates = req.body;

  await db.query(
    'UPDATE users SET ? WHERE id = ?',
    [updates, userId]
  );

  res.json({ success: true });
});

users表包含以下字段:

CREATE TABLE users (
  id           INT PRIMARY KEY,
  name         VARCHAR(100),
  email        VARCHAR(100),
  bio          TEXT,
  role         ENUM('user', 'moderator', 'admin') DEFAULT 'user',
  credits      INT DEFAULT 0,
  is_banned    BOOLEAN DEFAULT false
);

开发者的本意是让用户能够修改nameemailbio字段,但rolecreditsis_banned也在表中——因此查询会更新客户端发送的任何字段。

为什么这很重要

客户端发送的数据会直接被用于SQL查询。由于users表中也包含了这些字段,因此查询机制并不知道开发者“真正想要修改哪些字段”。

如果有人发送如下数据:

{
  "name": "Alice",
  "role": "admin",
  "credits": 100000,
  "is_banned": false
}

那么这个人就会自动成为管理员,解除自己的封禁状态,并获得十万的积分。

如何修复这种漏洞

应该使用明确的允许列表,逐个字段地构建需要更新的对象,从而避免这种安全问题。

// Express.js – 安全版本
app.put('/api/users/me', authenticate, async (req, res) => {
  const userId = req.user.id;

  // 只有这些字段才能被用户修改
  const ALLOWED_fields = ['name', 'email', 'bio'];
  const updates = {};

  for (const field of ALLOWED_fields) {
    if (req.body[field] !== undefined) {
      updates[field] = req.body[field];
    }
  }

  if (Object.keys(updates).length === 0) {
    return res.status(400).json({ error: '没有提供有效的字段' });
  }

  // 对各个字段进行验证
  // isValidEmail是一个简单的辅助函数,用于检查电子邮件格式是否合法:^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
  if (updates.email && !isValidEmail(updates.email)) {
    return res.status(400).json({ error: '电子邮件格式无效' });
  }

  if (updates.name && (typeof updates.name !== 'string' || updates.name.length > 100)) {
    return res.status(400).json({ error: '名称必须是长度不超过100个字符的字符串' });
  }

  await db.query(
    'UPDATE users SET ? WHERE id = ?,'
    [updates, userId]
  );

  res.json({ success: true });
});

对于那些确实需要更新敏感字段的管理操作,应使用专门的接口端点,并为该端点设置独立的授权机制:

// 仅管理员可使用的用于修改用户角色的接口
app.put('/api/admin/users/:id/role', authenticate, requireRole('admin'), async (req, res) => {
  if (!/^\d+$/.test(req.params.id)) {
    return res.status(400).json({ error: '无效的用户ID' });
  }

  const userId = Number(req.params.id);
  const { role } = req.body;

  const VALID_ROLES = ['user', 'moderator', 'admin'];

  if (!VALID_ROLES.includes(role)) {
    return res.status(400).json({ error: '无效的角色' });
  }

  await db.query(
    'UPDATE users SET role = ? WHERE id = ?,'
    [role, userId]
  );

  res.json({ success: true });
});

切勿 将 req.body 直接用于数据库查询中,而应该逐个字段地构建更新对象。

3. 原型污染

当不可信的数据被递归地合并到某个对象中时,就会发生原型污染。这种机制会让攻击者有机会向 Object.prototype(所有普通 JavaScript 对象都继承自的这个基对象)中注入额外的属性。

OWASP Top 10 将这一安全漏洞归类为“软件与数据完整性故障(A08:2021)”

如何识别代码中的这一漏洞

下面是一个用于处理用户输入设置的配置合并函数:

// 存在安全风险的递归合并函数
function mergeConfig(target, source) {
  for (const key of Object.keys(source)) {
    if (typeof source[key] === 'object' && source[key] !== null) {
      if (!target[key]) target[key] = {};
      mergeConfig(target[key], source[key]); // 递归调用
    } else {
      target[key] = source[key]; // 通过方括号语法触发 __proto__ 的设置操作
    }
  }
}

app.post('/api/settings', authenticate, (req, res) => {
  const userSettings = {};
  mergeConfig(userSettings, req.body); // 合并不可信的输入数据
  applySettings(userSettings);
  res.json({ success: true });
});

为什么这很重要

攻击者会发送如下请求体:

{
  "__proto__": {
    "isAdmin": true
  }
}

递归调用的 mergeConfig 函数会到达 __proto__ 这个键,并执行 target '__proto__']['isAdmin'] = true 这一行代码。由于 __proto__ 是 JavaScript 的原型访问机制,因此这条指令会直接修改 Object.prototype。合并操作完成后:

const anyObject = {};
console.log(anyObject.isAdmin); // 输出 true — 这个值是从 Object.prototype 继承而来的

此时,运行中的应用程序中所有普通对象都会继承 isAdmin: true 这一属性。如果某个授权检查机制使用了这样的逻辑……

if (user.isAdmin) { /* 授予管理员权限 */ }

现在,这一检查对于所有用户来说都是有效的,无论他们的实际角色是什么。

如何修复这一漏洞

应在服务器端存储用户状态,并对每个字段进行单独验证。切勿递归地合并不可信的输入:

// Express.js - 安全版本
app.post('/api/settings', authenticate, async (req, res) => {
  // 从数据库中获取当前设置——绝不要从客户端获取
  const current = await db.query(
    'SELECT theme, language, notifications FROM user_settings WHERE user_id = ?,'
    [req.user.id]
  );

  // 根据允许的列表验证每个字段
  const safeSettings = {
    theme: validateEnum(req.body.theme, ['light', 'dark'], currenttheme),
    language: validateEnum(req.body.language, ['en', 'es', 'fr', 'de'], currentlanguage),
    notifications: typeof req.body.notifications === 'boolean'
      ? req.bodynotifications
      : currentNotifications
  };

  await db.query(
    'UPDATE user_settings SET ? WHERE user_id = ?,'
    [safeSettings, req.user.id]
  );

  res.json({ success: true });
});

function validateEnum(value, allowed, defaultValue) {
  return allowed.includes(value) ? value : defaultValue;
}

如果需要使用合并工具,可以使用Object.create(null)作为基础对象——该对象没有原型,因此__proto__属性不会被污染——并且要明确指定允许的键值对:

// 安全的合并方法:使用没有原型的对象
function safeMerge(allowedKeys, source) {
  const result = Object.create(null); // 没有原型,因此不会发生污染

  for (const key of allowedKeys) {
    if (key in source && typeof source[key] !== 'object') {
      result[key] = source[key];
    }
  }

  return result;
}

规则:

  • 切勿递归地合并不可信的输入到普通对象中。

  • 应将应用程序的状态存储在服务器端,不要依赖客户端来保存这些状态。

  • 对于那些会包含不可信键值对的数据容器,应使用Object.create(null)来创建。

  • 在使用任何字段之前,都要根据其数据类型和允许的值来进行验证。

4. 竞态条件

竞态条件之所以难以处理,是因为代码本身并没有错误。问题出在执行时间上:当两个请求几乎同时到达、都检查了相同的条件、都得到了有效的结果后,就会发生这种问题。本来这种操作应该只发生一次,但却发生了两次。

这种现象被称为“检查时状态与使用时状态不一致”的问题:你检查时的系统状态已经发生了变化,因此后续的操作会基于错误的状态进行。

如何在代码中识别这种模式

以一次性使用的优惠券兑换接口为例:

// Express.js – 存在安全漏洞
app.post('/api/redeem-coupon', authenticate, async (req, res) => {
  const { couponCode } = req.body;
  const userId = req.user.id;

  // 第一步:检查优惠券是否仍然有效
  const coupon = await db.query(
    'SELECT id, discount_amount, used FROM coupons WHERE code = ? AND used = false',
    [couponCode]
  );

  if (!coupon) {
    return res.status(400).json({ error: '无效或已被使用的优惠券' });
  }

  // 在执行第三步之前,可能会有其他请求先通过第一步的验证。

  // 第二步:应用折扣优惠
  await applyDiscountToOrder(userId, coupon.discount_amount);

  // 第三步:将优惠券标记为已使用状态
  await db.query(
    'UPDATE coupons SET used = true, used_by = ? WHERE code = ?,',
    [userId, couponCode]
  );

  res.json({ success: true });
});

为何这很重要

几乎同时,有两份请求使用了相同的优惠券代码。这两份请求都先进入了第一步处理流程,但都没有进入第三步。在检查过程中,两者都被判定为“未使用”。然而,它们最终都享受到了折扣优惠——也就是说,同一张优惠券被重复使用了一次。

在任何需要先读取数据再写入数据的场景中,都可能存在类似的问题:在扣除金额之前会先检查余额,在进行预订之前会先核对库存数量,在增加投票数之前也会先验证投票信息。这些情况都可以以同样的方式被利用来制造漏洞。

如何修复这一漏洞

应将“先检查后执行”的处理模式改为原子操作。原子数据库更新能够确保条件判断和数据写入这两个操作作为一个不可分割的整体来完成:

// Express.js – 安全编程示例
app.post('/api/redeem-coupon', authenticate, async (req, res) => {
const { couponCode } = req.body;
const userId = req.user.id;

const connection = await db.getConnection();

try {
await connection.beginTransaction();

// 原子操作:只有其中一个请求能够更新那些“used = false”的记录。
// 数据库的行锁定机制能确保只有一个请求能够成功完成操作。
const [result] = await connection.query(
`UPDATE coupons
SET used = true, used_by = ?, used_at = NOW()
WHERE code = ? AND used = false`,
[userId, couponCode]
);

if (result.affectedRows === 0) {
await connection.rollback();
return res.status(400).json({ error: '无效或已被使用的优惠券' });
}

const [couponRows] = await connection.query(
'SELECT discount_amount FROM coupons WHERE code = ?',
[couponCode]
);
const coupon = couponRows[0];

await applyDiscountToOrder(userId, coupon.discount_amount, connection);

await connection.commit();

res.json({ success: true, discount: coupon_discount_amount });
} catch (error) {
await connection.rollback();
console.error('优惠券兑换失败:', error);
res.status(500).json({ error: '无法处理该优惠券' });
} finally {
connection.release();
}
});

关键在于`UPDATE ... WHERE code = ? AND used = false`这一句代码。在更新操作进行期间,数据库会锁定相关记录,因此只有其中一个请求能够成功完成操作。第二个请求会收到“affectedRows = 0”的反馈,从而正确地判断出操作失败。

任何需要在写入数据之前先读取数据来做出决策的场景,都可能存在竞态条件风险。因此,必须确保这些检查操作和数据写入操作是原子性的。

5. 业务逻辑缺陷

业务逻辑缺陷是最难被发现的漏洞类型。自动化扫描工具无法识别它们,代码审查也可能会忽略这些问题,因为这些代码本身运行起来确实是正确的。问题的根源在于代码的设计目的,而不是其实现方式。

最常见的错误就是:假设客户端会发送合理的数值数据。

如何在你的代码中识别这种漏洞

一个电子商务结算端点:

// Express.js — 存在安全漏洞
app.post('/api/checkout', authenticate, async (req, res) => {
  const { items } = req.body;

  let total = 0;
  const processedItems = [];

  for (const item of items) {
    const product = await db.query(
      'SELECT id, price FROM products WHERE id = ?,'
      [itemproductId]
    );

    if (!product) {
      return res.status(400).json({ error: `产品未找到:${item productId}` });
    }

    // 相信客户端提供的数量值
    const itemTotal = product.price * item.quantity;
    total += itemTotal;

    processedItems.push({productId: product.id, quantity: item_quantity, price: product.price });
  }

  if (total > 100) {
    total = total * 0.9; // 打九折
  }

  await createOrder(req.user.id, processedItems, total);
  res.json({ success: true, total });
});

为什么这很重要

问题1 — 数量为负值:该代码直接将服务器提供的价格与客户端输入的数量相乘,而没有检查这个数量是否为正数。如果用户为某件价格较高的商品输入“quantity: -5”,那么这一项对总金额的计算结果将会是负数,从而导致最终的总金额减少。

问题2 — 浮点数运算:在JavaScript中,0.1 + 0.2的结果实际上是0.30000000000000004。如果不进行四舍五入,财务计算过程中会逐渐累积误差。

问题3 — 折扣操纵:如果系统提供了在结算后修改订单的接口,且不重新计算总金额,那么用户就可以通过先添加商品来享受折扣,然后再删除部分商品,从而继续保持已享受折扣后的价格。

如何修复这一安全漏洞

必须对所有数值输入进行验证,并在服务器端重新计算所有总金额。对于金钱相关的计算,应使用整数运算来避免浮点数误差:

// Express.js — 加强安全性
app.post('/api/checkout', authenticate, async (req, res) => {
  const { items } = req.body;

  if (!Array.isArray(items) || items.length === 0) {
    return res.status(400).json({ error: '购物车中至少需要包含一件商品' });
  }

  if (items.length > 50) {
    return res.status(400).json({ error: '购物车中不能超过50件商品' });
  }

  let totalCents = 0; // 使用整数运算可避免浮点数误差
  const processedItems = [];

  for (const item of items) {
    if (!/^\d+$/.test(String(itemproductId))) {
      return res.status(400).json({ error: `无效的产品ID:${item productId}` });
    }

    const productId = Number(item.productId);

    // 验证数量:必须是由数字组成的字符串,且数值在1到10之间
    if (!/^\d+$/.test(String(item.quantity))) {
      return res.status(400).json({
        error: `产品${productId}的数量无效`
      });
    }

    const quantity = Number(itemquantity);

    if (quantity < 1 || quantity > 10) {
      return res.status(400).json({
        error: `产品${productId}的数量必须在1到10之间`
      });
    }

    const product = await db.query(
      'SELECT id, name, price_cents, stock FROM products WHERE id = ? AND active = true',
      [productId]
    );

    if (!product) {
      return res.status(400).json({ error: `产品未找到:${productId}` });
    }

    if (product.stock < quantity) {
      return res.status(400).json({
        error: `商品${product.name}的库存不足`
      });
    }

    // 使用服务器提供的价格——切勿相信客户端的价格信息
    totalCents += product.price_cents * quantity;

    processedItems.push({
      productId: product.id,
      name: product.name,
      quantity,
      unitPriceCents: product.price_cents
    });

    // 计算折扣时使用整数运算
    const discountMultiplier = totalCents > 10000 ? 90 : 100; // 10000分等于100美元
    const finalTotalCents = Math.round(totalCents * discountMultiplier / 100);

    await createOrder(req.user.id, processedItems, finalTotalCents);

    res.json({
      success: true,
      total: (finalTotalCents / 100).toFixed(2),
      currency: 'USD'
    });
});

发生了哪些变化,以及原因是什么:

  • 使用整数进行运算可以避免浮点数计算带来的误差。例如,10000 + 3000这样的运算结果一定是精确的。

  • quantity < 1 || quantity > 10这一限制可以有效防止出现负数量或订单金额过大的情况。

  • items.length > 50这一规则可以避免发送过大的请求数据。

  • product.stock < quantity这一条件能确保订单量不会超过现有的库存数量。

  • 所有的总价、折扣以及最终价格都是由服务器根据服务器端存储的价格来计算的。

需要为每一个数值输入设定有效的范围,并且所有总额都需要在服务器端重新计算。因为客户端提供的价格或数量信息并不可信。

6. JWT配置错误

在Node.js API中,JWT被广泛使用,而jsonwebtoken这个库也使得使用JWT变得非常方便。不过,这种便利性既有好处也有坏处:正确使用JWT确实很简单,但同时也很容易被误用。

OWASP Top 10将认证失败问题归类为身份识别与认证失败(A07:2021)。其中,有三种配置错误会反复出现。

如何在自己的代码中检测这种漏洞

错误1 — 在verify()方法中未指定算法:

// 存在安全隐患
const decoded = jwt.verify(token, process.env.JWT_SECRET);

如果不在配置中指定algorithms选项,某些JWT实现可能会被欺骗,从而接受那些在头部字段中声明"alg": "none"的令牌——这意味着根本不需要进行签名验证。

错误2 — 使用弱密钥或硬编码的密钥:

 存在安全隐患
const token = jwt.sign({ userId: user.id }, 'secret');

如果攻击者获得了有效的令牌,那么长度较短且容易被预测的HS256密钥就有可能被暴力破解。

错误3 — 在JWT的有效载荷中包含敏感数据:

 存在安全隐患
const token = jwt.sign({
  userId: user.id,
  passwordHash: user.passwordHash, // 绝对不要这样做
  role: user.role
}, secret);

JWT的有效载荷只是经过Base64编码,并没有被加密。因此,任何持有该令牌的人都可以解码其中的内容。

如何解决这些问题

 安全的JWT实现方式
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// 一次性生成一个强密钥,并将其存储为环境变量:
// node -e "console.log(crypto.randomBytes(64).toString('hex'))"
const JWT_SECRET = process.env.JWT_SECRET;

if (!JWT_SECRET || Buffer.from(JWT_SECRET, 'hex').length < 32) {
  throw new Error('JWT_SECRET必须至少由32个随机字节组成');
}

function signToken(userId) {
  return jwt.sign(
    { sub: userId },         // ‘sub’是用于表示用户身份的标准字段
    JWT_SECRET,
    {
      algorithm: 'HS256',    // 必须明确指定算法
      expires: '15m',      // 使用有效期较短的令牌可以减少被窃取后造成的风险
      issuer: 'your-app-name',
      audience: 'your-app-name'
    }
  );
}

function verifyToken(token) {
  return jwt.verify(token, JWT_SECRET, {
    algorithms: ['HS256'],   // 只允许使用预期的算法
    issuer: 'your-app-name',
    audience: 'your-app-name'
  });
}

function authenticate(req, res, next) {
  const authHeader = req.headersauthorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: '未提供令牌' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = verifyToken(token);
    req.user = { id: decoded.sub };
    next();
  } catch (err) {
    return res.status(401).json({ error: '令牌无效或已过期' });
  }
}

关于令牌撤销的说明:JWT是一种无状态机制,这意味着服务器不会保存这些令牌的任何记录。因此,如果没有额外的基础设施支持,你在用户登出或账户被入侵后无法立即使令牌失效。

常见的解决方案包括使用有效期较短的访问令牌(例如15分钟),并将刷新令牌存储在服务器端;或者使用Redis这样的快速存储系统来维护一个令牌黑名单。请根据你的应用程序需求选择合适的方案。

JWT安全检查清单:

  • 在调用`verify()`方法时,务必传入`algorithms: ['HS256']`参数。

  • 使用的密钥长度应至少为32个随机字节,建议使用`crypto.randomBytes`生成该密钥。

  • 为令牌设置较短的过期时间。

  • 在JWT的有效载荷中仅存储非敏感信息(如用户ID),切勿包含电子邮件地址、密码或角色信息。

  • 务必使用标准的 JWT声明字段,例如`sub`、`iss`、`aud`和`exp`。

  • 在正式投入生产环境之前,先制定好令牌撤销策略。

总结

以下是本教程中涉及的六类安全漏洞的简要概述:

>

漏洞类型 根本原因 核心解决方法
IDOR漏洞 未进行授权检查 需从已认证的会话中验证用户身份
批量赋值漏洞 所有请求体字段都会被写入数据库 应明确指定允许被写入的字段
原型污染漏洞 对不可信输入进行了递归合并操作 需在服务器端保存状态信息,合并操作时使用`Object.create(null)`创建对象
竞态条件漏洞 先检查后执行操作,但缺乏原子性保障 应使用原子的`UPDATE ... WHERE condition`语句或事务机制来解决问题
业务逻辑缺陷 盲目信任客户端提供的数值信息 应对所有数值输入进行范围验证;处理财务相关操作时必须保证原子性
JWT配置错误 未设置允许使用的加密算法,密钥强度不足 应明确指定加密算法,使用强随机生成的密钥,并为令牌设置较短的过期时间

这些漏洞其实并不需要攻击者具备高超的技巧——他们只需要利用那些在错误的时间、错误的场景下被信任的代码即可实施攻击。

以下是一些值得牢记的关键原则:始终从会话中获取授权信息,而不要从请求数据中获取;对所有数值输入进行范围验证;确保财务相关操作的原子性;并且要明确配置JWT的相关参数。

了解如何修复这些漏洞只是解决问题的一半,更重要的是要明白在实际的安全评估过程中,这些漏洞是如何被发现和被利用的。

如果你想进一步深入了解攻击者的思维方式——比如渗透测试人员是如何针对Web应用程序发起攻击的,他们会寻找哪些漏洞,以及如何利用多个漏洞来达成攻击目标——那么这篇关于Web应用程序攻击技巧的指南会为你提供详细的解答。

Comments are closed.