You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React与Express中如何防范OTP验证的响应篡改漏洞

修复2FA响应篡改绕过漏洞的方案

核心问题分析

当前漏洞的根源是:OTP验证失败时后端仍返回200 HTTP状态码,前端仅依赖响应体中的success字段判断登录结果,攻击者可通过拦截响应篡改success为true,直接绕过OTP验证。


后端(Express)修复措施

1. 严格区分HTTP状态码

验证失败时返回4xx类错误状态码(如401 Unauthorized),而非始终返回200:

// /verifyAndLogin 接口示例修改
app.post('/verifyAndLogin', async (req, res) => {
  const { email, password, otp } = req.body;
  
  // 1. 重复验证账号密码,避免前端直接跳过登录调用此接口
  const user = await User.findOne({ email });
  if (!user || !await bcrypt.compare(password, user.password)) {
    return res.status(401).json({
      success: false,
      message: "账号或密码错误"
    });
  }

  // 2. 验证OTP(仅对启用2FA的用户校验)
  if (user.twoFactorEnabled && !verifyOTP(user.otpSecret, otp)) {
    // OTP错误时返回401,而非200
    return res.status(401).json({
      success: false,
      message: "无效的OTP"
    });
  }

  // 3. 验证通过,返回JWT和200状态
  const token = generateJWT(user);
  res.status(200).json({
    success: true,
    redirect: true,
    message: "登录成功",
    token: token
  });
});

2. 增加临时会话校验,禁止直接调用验证接口

在/login接口验证账号密码后,生成一次性临时会话标识(存入Redis并设置5分钟过期),前端调用/verifyAndLogin时必须携带该标识,后端先校验标识有效性再处理OTP:

// /login 接口修改
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });
  
  if (!user || !await bcrypt.compare(password, user.password)) {
    return res.status(401).json({ success: false, message: "账号或密码错误" });
  }

  if (user.twoFactorEnabled) {
    // 生成临时会话ID,存入Redis
    const tempSessionId = crypto.randomUUID();
    await redis.setEx(`temp_login:${tempSessionId}`, 300, user._id.toString());
    
    // 发送OTP到邮箱
    await sendOTP(user.email);
    
    return res.status(200).json({
      success: true,
      needOTP: true,
      tempSessionId: tempSessionId // 前端需携带此ID调用验证接口
    });
  } else {
    // 未启用2FA,直接返回JWT
    const token = generateJWT(user);
    res.status(200).json({ success: true, redirect: true, token: token });
  }
});

// /verifyAndLogin 接口修改
app.post('/verifyAndLogin', async (req, res) => {
  const { tempSessionId, otp } = req.body;
  
  // 先校验临时会话ID有效性
  const userId = await redis.get(`temp_login:${tempSessionId}`);
  if (!userId) {
    return res.status(403).json({ success: false, message: "会话已过期或无效" });
  }

  const user = await User.findById(userId);
  if (!user || !verifyOTP(user.otpSecret, otp)) {
    // 删除无效会话
    await redis.del(`temp_login:${tempSessionId}`);
    return res.status(401).json({ success: false, message: "无效的OTP" });
  }

  // 验证通过,删除临时会话并返回JWT
  await redis.del(`temp_login:${tempSessionId}`);
  const token = generateJWT(user);
  res.status(200).json({
    success: true,
    redirect: true,
    message: "登录成功",
    token: token
  });
});

3. 移除重复密码传输

通过临时会话ID关联用户,无需在/verifyAndLogin接口重复提交密码,降低密码泄露风险。


前端(React)修复措施

1. 优先校验HTTP状态码

前端判断登录结果时,先检查响应状态码,再处理响应体:

// OTP验证请求示例
const verifyOTP = async (tempSessionId, otp) => {
  try {
    const response = await fetch('/verifyAndLogin', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ tempSessionId, otp })
    });

    // 非200状态直接判定失败
    if (!response.ok) {
      const errorData = await response.json();
      alert(errorData.message || "验证失败");
      return;
    }

    const data = await response.json();
    if (data.success) {
      // 登录成功,存储token并跳转
      localStorage.setItem('token', data.token);
      window.location.href = '/dashboard';
    } else {
      alert(data.message);
    }
  } catch (error) {
    alert("网络错误,请重试");
  }
};

2. 路由守卫限制OTP页面访问

添加路由守卫,仅当用户持有有效临时会话ID时,才能进入OTP验证页面:

// 路由守卫组件
const OTPGuard = ({ children }) => {
  const tempSessionId = localStorage.getItem('tempLoginSession');
  if (!tempSessionId) {
    // 无临时会话,跳转到登录页
    return <Navigate to="/login" />;
  }
  return children;
};

// 路由配置
<Routes>
  <Route path="/login" element={<LoginPage />} />
  <Route path="/verify-otp" element={
    <OTPGuard>
      <OTPVerificationPage />
    </OTPGuard>
  } />
  <Route path="/dashboard" element={<DashboardPage />} />
</Routes>

3. 登录成功后清除临时会话

验证通过后立即删除存储的临时会话ID,避免重复使用:

// 登录成功处理逻辑
if (data.success) {
  localStorage.setItem('token', data.token);
  localStorage.removeItem('tempLoginSession'); // 清除临时会话
  window.location.href = '/dashboard';
}

额外安全建议

  • 设置OTP较短过期时间(如5分钟),验证失败后立即失效,防止暴力破解。
  • 对多次OTP验证失败的用户,暂时锁定账号或增加图形验证码校验。
  • 强制使用HTTPS传输所有请求,防止数据被窃听或篡改。

内容的提问来源于stack exchange,提问作者Milind Modi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 00:39:56