PAYATHON 2026

使用 JS 是否会影响网站安全并泄露 API keys/secrets?

支付阿杰

明确结论

JavaScript 本身不会让网站变得不安全。风险主要取决于代码在哪里运行。

浏览器端的 JavaScript、打包后的前端代码,以及浏览器发出的网络请求,用户都能查看和修改。因此,只要 API key、token、私钥或服务端密钥出现在前端,就应视为已经公开。代码压缩、混淆或隐藏环境变量名都无法保护这些秘密。

服务端 JavaScript(如 Node.js)可以安全地使用密钥,但密钥必须始终留在服务端,不能通过接口响应、日志、错误信息或构建产物泄露。

哪些内容可以出现在前端

有些服务会提供专门用于浏览器的公开标识,例如:

  • 支付平台的 publishable key
  • OAuth client ID
  • Firebase 等服务的客户端配置
  • CAPTCHA site key

这些值通常只用于标识项目,不会直接授予敏感权限。能否公开,仍要以相应服务商的文档为准。

以下内容一般不能放在前端:

  • Payment secret key
  • SMS API secret
  • OAuth client secret
  • 数据库密码
  • JWT signing secret
  • 云服务 Access Key Secret
  • Webhook signing secret
  • 具有写入、退款、发短信或管理权限的 token

判断一个值是否属于秘密,不能只看变量名,还要看拿到它的人能执行哪些操作。

正确的集成方式

浏览器不应携带密钥直接调用 SMS、支付管理或其他敏感 API。正确的数据流是:

Browser → Your backend → Third-party API

前端只向自己的后端发送业务所需的数据。后端完成身份验证、权限检查、参数校验和频率限制,再使用服务端密钥调用第三方服务。

以发送短信为例,前端可以调用自己的接口:

async function requestVerificationCode(phone) {
  const response = await fetch("/api/send-code", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    credentials: "include",
    body: JSON.stringify({ phone }),
  });

  if (!response.ok) {
    throw new Error("Failed to request verification code");
  }

  return response.json();
}

Node.js 后端从环境变量读取密钥,然后调用 SMS 服务:

import express from "express";

const app = express();
app.use(express.json());

app.post("/api/send-code", requireAuthenticatedUser, async (req, res) => {
  const { phone } = req.body;

  if (!isValidPhoneNumber(phone)) {
    return res.status(400).json({ error: "Invalid phone number" });
  }

  // 生产环境还应加入验证码、人机校验、频率限制和审计记录。
  const response = await fetch("https://sms-provider.example/v1/messages", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.SMS_API_SECRET}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      to: phone,
      message: "Your verification code is 123456",
    }),
  });

  if (!response.ok) {
    return res.status(502).json({ error: "SMS provider request failed" });
  }

  return res.status(202).json({ accepted: true });
});

这里的域名、字段和认证方式仅用于说明整体结构。实际参数必须按照所用 SMS 服务的文档填写。验证码也不能像示例中这样写死,应安全生成,并限制有效期和尝试次数。

不要把密钥注入前端构建

一种常见错误是把密钥写入前端环境变量:

VITE_PAYMENT_SECRET=secret_value
NEXT_PUBLIC_SMS_API_KEY=secret_value
REACT_APP_AUTH_SECRET=secret_value

VITE_、NEXT_PUBLIC_、REACT_APP_ 等前缀通常意味着变量会被打包进浏览器代码。部署后,用户可以在 JavaScript 文件或网络请求中找到它们。

服务端环境变量不应使用公开前缀:

SMS_API_SECRET=...
PAYMENT_API_SECRET=...
JWT_SIGNING_SECRET=...

服务端可以这样读取密钥:

const paymentSecret = process.env.PAYMENT_API_SECRET;

if (!paymentSecret) {
  throw new Error("PAYMENT_API_SECRET is not configured");
}

在生产环境中,最好使用云平台或部署系统提供的 secret manager,不要把 .env 文件提交到仓库。

.gitignore 至少应包含:

.env
.env.*
!.env.example

.env.example 只保留变量名,不填写真实值:

SMS_API_SECRET=
PAYMENT_API_SECRET=
JWT_SIGNING_SECRET=

支付 API 的特殊注意事项

支付服务通常会同时提供公开 key 和 secret key。公开 key 可以在浏览器中初始化支付组件,secret key 必须保存在服务端。

金额、商品、折扣和订单状态也不能完全相信前端提交的值。例如,不能直接使用浏览器传来的金额创建订单:

// 不安全:攻击者可以修改 amount。
const { amount } = req.body;
await paymentClient.createPayment({ amount });

服务端应根据可信的商品数据重新计算金额:

const { productId, quantity } = req.body;

const product = await database.products.findById(productId);
if (!product || !Number.isInteger(quantity) || quantity < 1) {
  return res.status(400).json({ error: "Invalid order" });
}

const amount = product.priceInCents * quantity;

const payment = await paymentClient.createPayment({
  amount,
  currency: "CNY",
  metadata: {
    userId: req.user.id,
    productId,
  },
});

支付是否成功,也应由服务端验证。处理 webhook 时,必须校验平台提供的签名,不能因为客户端声称”支付成功”就更新订单状态。

Auth service 也不能只依赖前端

前端登录界面和客户端 SDK 都可以被修改,因此,真正的鉴权必须由服务端或可信服务的安全规则执行。

后端收到 access token 后,应按照认证服务的要求检查:

  • 签名是否合法
  • iss 是否正确
  • aud 是否匹配当前应用
  • token 是否过期
  • 用户是否具有执行该操作所需的角色或权限

隐藏按钮不等于权限控制。即使普通用户看不到”删除”按钮,也可能手动调用删除接口。

认证服务提供的浏览器公开配置未必是秘密,但数据库安全规则、允许的来源、回调地址和服务端授权仍然需要正确配置。

Packages 会不会窃取密钥

有可能,但要区分依赖包的运行环境。

前端依赖包本来就能接触页面数据,也可能读取用户输入、token 和网络响应。一旦依赖遭到供应链攻击,恶意代码就可能在浏览器中窃取这些信息。

服务端依赖的风险更高。运行中的 Node.js 包通常与应用进程拥有相同权限,可以读取环境变量、文件和网络数据。有些包还会在安装阶段通过安装脚本执行代码。

可以通过以下措施降低风险:

  • 尽量减少依赖,不要为了简单功能安装来源不明的包。
  • 检查包的维护状态、源码仓库、发布历史和实际用途。
  • 提交 lockfile,并在 CI 中使用锁定依赖的安装方式,例如 npm ci。
  • 定期运行 npm audit,同时留意所用包的安全公告。
  • 使用依赖更新工具,但不要在未经测试的情况下自动部署重大更新。
  • 限制 CI、构建环境和生产进程能够访问的密钥。
  • 分开管理构建阶段和生产运行阶段的密钥。
  • 为高权限密钥设置最小权限、来源限制、额度限制和轮换策略。
  • 如果业务允许,可以评估是否禁用安装脚本:
npm ci --ignore-scripts

有些依赖确实需要安装脚本。使用该选项前,必须确认项目仍能正常构建和运行。

锁定版本可以减少意外升级的风险,但不能证明某个版本一定安全。

还需要防范 XSS 和 token 泄露

即使所有 API secret 都保存在后端,跨站脚本攻击(XSS)仍可能窃取用户会话,或借用户身份发起操作。

建议采取以下措施:

  • 不要把未经处理的用户内容插入 innerHTML。
  • 使用框架默认的文本转义机制。
  • 必须渲染 HTML 时,使用经过审查的 HTML sanitizer。
  • 配置合理的 Content Security Policy(CSP)。
  • 为会话 cookie 设置 HttpOnly、Secure 和合适的 SameSite 属性。
  • 不要通过 URL 查询参数传递密钥或长期 token。
  • 不要在控制台、分析系统或错误日志中记录密钥。

Cookie 示例:

Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax; Path=/

HttpOnly 可以降低 JavaScript 直接读取 cookie 的风险,但无法解决所有 XSS 问题。恶意脚本仍可能利用当前会话发起请求。

如果密钥已经放进前端或 Git 仓库

仅删除代码或提交记录还不够。已经公开过的密钥应视为泄露,并按以下顺序处理:

  1. 立即在服务商后台撤销或轮换密钥。
  2. 检查调用日志、账单、短信发送记录、支付操作和异常登录。
  3. 创建权限更小的新密钥。
  4. 将新密钥存入服务端的 secret manager 或受保护的环境变量。
  5. 调整架构,让浏览器只调用自己的后端。
  6. 清理仓库历史和构建产物,但不能用清理历史代替密钥轮换。
  7. 为用量、费用和异常请求配置告警。

最终判断原则

判断方法很简单:如果某个值已经发给浏览器,就不能再把它当作秘密。

JavaScript 可以安全地用于前端和后端,但 SMS secret、payment secret、签名密钥等敏感信息必须只存在于可信的服务端环境。同时,还要通过服务端鉴权、输入校验、频率限制、依赖审查、最小权限和密钥轮换,减少接口滥用与供应链攻击的风险。

备注:内容仅供参考。