← Blog
·10 blog.minutes

安全编码:开发者编写安全代码指南

安全不是运维团队的工作。你写的每一行代码要么打开一扇门,要么把它锁上。以下是每个开发者在 2026 年需要了解的编写安全代码的知识。

有一个危险的想法已经悄悄进入我们的行业:安全是别人的问题。属于运维团队、安全工程师、每季度来一次对应用进行渗透测试的专职测试人员。这个想法是错误的,它也是你每个星期二读到的大多数数据泄露的原因。

现实是,安全是在编辑器中决定的,而不是在安全评审中。你做的每个决定——如何构建 SQL 查询、如何处理用户输入、如何存储会话令牌——要么打开一扇门,要么把它锁上。攻击者只需要一扇开着的门。你需要关闭每一扇。

本文涵盖了每个开发者应该知道的基本安全编码实践。它借鉴了 2026 年的 OWASP Top 10,其中有了一些重大变化,并覆盖了日复一日出现在真实代码库中的漏洞:注入、跨站脚本、跨站请求伪造、认证失败、依赖风险和密钥暴露。每个部分都包含你今天就可以应用的具体、可执行的模式。

2026 年 OWASP Top 10:有什么变化以及为什么

OWASP Top 10 是我们的行业最接近关键 Web 应用安全风险共识列表的东西。2026 年版引入了几项反映威胁格局演变的显著变化。理解这些变化有助于你将精力集中在实际重要的风险上。

2026 年最大的变化是将 AI 生成的代码提升为一个独立的风险类别。OWASP 首次明确承认大型语言模型生成的代码引入了独特的安全挑战。这些模型在大量公共代码语料库上训练,其中包括从维护良好的库到开发者粘贴的 Stack Overflow 代码片段的一切。AI 重现了它见过的模式,包括有漏洞的那些。Synk 在 2025 年的一项研究发现,AI 助手在给出安全模糊的提示时,大约 30% 的时间建议了包含已知漏洞的代码。信息很明确:AI 生成的代码需要与不熟悉的承包商编写的代码同等水平的审查。

另一个重大变化是注入类别的合并。以前的版本将 SQL 注入、NoSQL 注入、操作系统命令注入和 LDAP 注入作为单独的条目处理。2026 版将它们归入一个称为“注入”的单一类别,反映了底层模式——不可信数据连接到解释器中——无论目标是什么都是相同的。这是一个有用的重新框架,因为它将注意力引向根因而不是具体变体。

密码学失败在列表中地位上升,这是由日益普遍的 AI 驱动密码分析和持续的量子邻近迁移规划灾难驱动的。如果你没有使用现代认证加密(AES-GCM、ChaCha20-Poly1305),如果你甚至还没有开始考虑长期数据的后量子迁移,你正在积累未来将以利息偿还的债务。

2026 年的列表还将安全配置错误和软件完整性失败合并到一个更广泛的类别中,称为“配置与受损”,认识到许多完整性失败——例如使用不可信的基础镜像或未签名的依赖——从根本上来说是在开发周期早期做出的配置决策。

安全不是产品或功能。它是工程文化的一个属性。如果你的团队在设计阶段不关心安全,到最后再多的扫描也无法修复它。

注入攻击:老故事,仍然是最大的问题

注入攻击仍然位于 OWASP 列表的顶部是有原因的:它们易于执行,影响毁灭性,并且在生产代码中出人意料地常见。根本问题是程序通过拼接包含用户控制输入的字符串来构造命令或查询。攻击者精心构造该输入以跳出预期上下文并注入他们自己的指令。

SQL 注入是典型的例子,而且它仍然有效。十年前在大学里学过 SQL 注入的开发者仍然在截止日期压力下编写有漏洞的代码。修复方法已被充分理解:参数化查询、预编译语句或正确处理转义的 ORM。

// Vulnerable: string concatenation with user input
const query = `SELECT * FROM users WHERE email = '${req.query.email}'`;
db.execute(query);

// Fixed: parameterized query
const query = 'SELECT * FROM users WHERE email = ?';
db.execute(query, [req.query.email]);

有漏洞的模式在任何有经验的开发者看来都应该明显是错误的,然而它每周都会出现在代码审查中。原因总是一样的:时间压力、一个看起来太简单不值得用参数化的查询、或者一个对特定数据库驱动不可用的 ORM。这些原因没有一个能在事故事后分析中幸存。

同样的原则适用于 NoSQL 数据库。例如,MongoDB 在用户输入直接传递到查询对象时容易受到注入。如果查询是从原始输入构建的,攻击者可以传递 { "$ne": "" } 作为密码值来完全绕过认证。

操作系统命令注入是另一个拒绝消亡的变体。使用用户控制输入调用 exec、spawn 或 child_process 是灾难性的。如果你需要运行外部命令,根据允许值的严格白名单验证输入。永远不要从用户输入构造命令字符串。

跨站脚本和 SPA 挑战

跨站脚本(XSS)是一种注入攻击,目标是浏览器的 DOM 而不是数据库。攻击者将恶意 JavaScript 注入到网页中,该脚本在受害者会话的上下文中执行,使其能够访问 cookies、localStorage、会话数据,并代表用户发出请求。

使用 React、Vue 和 Svelte 构建的单页应用的兴起显著改变了 XSS 格局。现代框架自动转义模板表达式中的值,这消除了最常见的 XSS 向量——但开发者仍然可以绕过这些保护。最常见的绕过是 React 中的 dangerouslySetInnerHTML、Vue 中的 v-html 或 Svelte 中的 @html。这些 API 的存在是为了合法的用例(从 CMS 渲染富文本),但如果内容未经过净化处理,它们就为 XSS 打开了大门。

// Vulnerable: rendering unsanitized HTML from user input
function Comment({ body }) {
  return <div dangerouslySetInnerHTML={{ __html: body }} />;
}

// Fixed: sanitize before rendering
import DOMPurify from 'dompurify';

function Comment({ body }) {
  const clean = DOMPurify.sanitize(body);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

服务端渲染为 XSS 保护增加了另一个维度。如果你的应用在服务器上渲染用户生成的内容并将 HTML 发送到客户端,该内容必须在服务端进行转义。许多模板引擎默认为转义输出(Handlebars、带转义的 EJS、Pug),但开发者可以通过像三重大括号 {{{ }}} 或管道操作符这样的辅助方法选择不转义的输出。审查模板中每个非转义输出的出现。

开发者经常忽略的第三个 XSS 向量是通过数据属性和 URL 的间接注入。如果你使用用户输入在 DOM 元素上设置数据属性,如果输入包含破坏属性上下文的字符,你可能是脆弱的。同样,如果你从用户输入构造 URL 并将其注入到锚标签中,攻击者可以使用 javascript: URL 来执行任意代码。

跨站请求伪造和认证强化

跨站请求伪造(CSRF)利用 Web 应用对已认证用户浏览器的信任。攻击者构造一个对你的应用的请求,并诱骗已认证用户执行它——通过访问恶意页面、点击链接甚至加载图片。如果你的应用仅依赖会话 cookies 进行认证,浏览器会自动在对你的域名的每个请求中包含这些 cookies,使 CSRF 攻击成为可能。

标准防御是 CSRF 令牌:一个唯一的、不可猜测的值嵌入在每个表单中并在服务器端验证。当用户提交表单时,服务器检查 CSRF 令牌是否与会话中存储的匹配。攻击者的伪造请求不能包含此令牌,因为同源策略阻止攻击者的页面读取你的服务器的响应。

// Vulnerable: no CSRF protection
app.post('/api/transfer', (req, res) => {
  const { toAccount, amount } = req.body;
  transferFunds(req.session.userId, toAccount, amount);
  res.json({ ok: true });
});

// Fixed: validate CSRF token
app.post('/api/transfer', (req, res) => {
  const token = req.headers['x-csrf-token'];
  if (!validateCsrfToken(req.session, token)) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }
  const { toAccount, amount } = req.body;
  transferFunds(req.session.userId, toAccount, amount);
  res.json({ ok: true });
});

现代框架开箱即用地包含 CSRF 保护。Express.js 中间件如 csurf(已弃用但仍在使用)、Django 内置的 CSRF 中间件和 Rails 的认证令牌机制都实现了相同的模式。开发者犯的错误是禁用不为 HTML 表单提供服务的 API 端点的 CSRF 保护,忘记了如果没有正确的 CORS 配置,这些端点仍然可能成为跨源请求的目标。

认证强化超越了 CSRF。每个会话应该有有效期。每个密码重置令牌应该是单次使用且限时的。每个登录端点应该实现限流以防止暴力攻击。多因素认证应该对所有用户可用并鼓励使用,对管理账户应该是必需的。

限流是你可以实现的最具成本效益的安全措施之一。它需要最少的代码,没有持续的维护负担,并防止了整类攻击:凭证填充、暴力、枚举攻击和许多形式的 API 滥用。在应用级别(每用户)、网络级别(每 IP)和端点级别(每敏感操作)实现限流。登录端点的一个合理起点是每分钟每用户五次尝试。

依赖管理和密钥处理

现代应用建立在开源依赖的基础上。一个典型的 Node.js 项目有数千个传递依赖。每个都是一个潜在的攻击向量。2024 年的 event-stream 事件——一个恶意行为者获得了维护者访问权限并将窃取加密货币的代码注入到一个流行的 npm 包中——不是异常——而是关于开源供应链系统性风险的警告。

  • 使用带完整性验证的包管理器。npm shrinkwrap 和 yarn.lock 确保每次安装使用完全相同的依赖树。锁定你的依赖,不要使用范围操作符。
  • 将依赖漏洞扫描作为 CI 管道的一部分运行。像 npm audit、Snyk、Dependabot 和 Trivy 这样的工具自动检测依赖图中的已知漏洞,并在存在关键漏洞时阻止构建。
  • 移除未使用的依赖。每个你未积极使用的包都是没有好处的负债。使用像 depcheck 这样的工具来识别 package.json 中的死重。
  • 审计每个依赖请求的权限。一个读取环境变量、访问文件系统和发出网络请求的包是有风险的。为每个权限提供理由。
  • 设置带有审阅流程的自动化依赖更新。Dependabot 或 Renovate 会在新版本可用时打开拉取请求。查看变更日志,检查破坏性变更,并及时合并——特别是对于安全补丁。

密钥管理是这个等式的另一半。在源代码中硬编码的 API 密钥、数据库密码和加密密钥是渗透测试中最常见的发现之一。它们也完全是可预防的。

// Vulnerable: hardcoded secrets in source code
const DB_PASSWORD = 'sup3r-s3cr3t-passw0rd!';
const API_KEY = 'sk-live-abc123def456';

// Fixed: use environment variables with validation
function getEnv(name: string): string {
  const value = process.env[name];
  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`);
  }
  return value;
}

const dbPassword = getEnv('DB_PASSWORD');
const apiKey = getEnv('API_KEY');

// Also: use .env files locally, never commit them
// Add .env to .gitignore from day one

如果你曾经将密钥提交到 git 仓库,它已经被泄露了。从最新提交中移除密钥是不够的——它仍然在 git 历史中。任何有仓库访问权限的人都能找到它。唯一安全的做法是立即轮换密钥,如果仓库是公开或共享的,使用 git-filter-repo 或 BFG Repo-Cleaner 等工具将其从历史中清除。

在生产中使用专门的密钥管理工具。HashiCorp Vault、AWS Secrets Manager、Azure Key Vault 或 Google Secret Manager 提供安全存储、访问审计、自动轮换和细粒度访问控制。环境变量对于本地开发是可接受的,但生产密钥应该在应用启动时从密钥存储中获取,而不是嵌入在环境配置中。

工具:SAST、DAST 和安全标头

安全编码不仅关乎你写什么——还关乎你检查什么以及你如何部署。最有纪律的开发团队将静态分析、动态分析和基础设施强化相结合,创建多层防御。

静态应用安全测试(SAST)工具在不执行代码的情况下分析源代码。它们寻找指示漏洞的模式:SQL 注入、XSS、不安全反序列化、硬编码密钥等。SAST 工具集成到开发工作流中,并在代码编写时提供反馈,这是修复成本最低的时候。流行的 SAST 工具包括 Semgrep、SonarQube、CodeQL 和带有安全插件的 ESLint。成功采用 SAST 的关键是修复噪声水平——调整规则使每个告警都可执行,否则开发者将学会忽略它们。

动态应用安全测试(DAST)工具通过发送精心设计的请求并观察响应来分析运行中的应用。它们检测 SAST 工具无法找到的漏洞,如 CSRF、认证绕过和配置问题。DAST 工具包括 OWASP ZAP、Burp Suite、Acunetix 和 StackHawk。DAST 在集成到 CI/CD 管道并在部署后针对预发布环境运行时最为有效。

第三层是通过安全标头进行基础设施强化。HTTP 响应标头告诉浏览器在渲染你的应用时如何行为。正确设置它们可以在没有运行时成本的情况下防止整类客户端攻击。

  • Content-Security-Policy:限制浏览器可以从哪些源加载资源。即使攻击者成功注入脚本标签,也能防止 XSS。
  • Strict-Transport-Security:强制浏览器对到你的域名的所有连接使用 HTTPS。防止 SSL 剥离攻击。
  • X-Frame-Options:防止你的应用被嵌入到其他域名的 iframe 中。缓解点击劫持攻击。
  • X-Content-Type-Options:防止浏览器 MIME 嗅探响应类型。降低通过错误标记的内容类型进行脚本注入的风险。
  • 带有 Secure、HttpOnly 和 SameSite 标志的 Set-Cookie:确保 cookies 仅通过 HTTPS 发送、JavaScript 无法访问且不跨源发送。SameSite=Lax 或 Strict 是最有效的单一 CSRF 防御。

安全标头易于配置且立即可见效。使用像 securityheaders.com 这样的工具扫描你的应用并识别缺失的标头。大多数应用框架支持通过中间件设置这些标头——Express.js 的 Helmet、Django 的安全中间件和 Rails 的 Rack::Protection 都维护良好且广泛使用。

开发时的 SAST、部署时的 DAST 和基础设施层的安全标头相结合,创造了重叠的覆盖范围。如果一层漏掉了一个漏洞,另一层可能会捕获它。这就是应用于软件开发生命周期的纵深防御。

建立安全文化

没有工具、检查清单或框架可以取代一个真正内化安全作为设计约束的开发团队。编写安全代码的团队和不这样做的团队之间的区别不是他们的静态分析工具的质量——而是嵌入在他们日常工作流程中的一组习惯和假设。

最重要的习惯是在实现之前进行威胁建模。在为一个新功能写一行代码之前,问:攻击者试图做什么?这个功能接触到的最有价值的数据是什么?如果攻击者控制输入会发生什么?如果数据库被入侵会怎样?尽早回答这些问题会暴露出任何代码审查都无法捕获的设计级漏洞,因为漏洞在架构中,而不是在实现中。

第二个习惯是将安全作为一等关注点的同行评审。只关注风格、正确性和性能的代码审查会遗漏安全问题。在你的拉取请求模板中添加一个安全检查清单项:SQL 注入?XSS?CSRF?差异中的密钥?授权检查?限流?通过使安全成为审阅过程的一部分,你将责任分散到整个团队,并在问题到达生产之前捕获它们。

第三个习惯是从事件中学习。当安全漏洞被发现时——无论是在你的代码库中还是其他人的——把它当作教学机会。进行一个专注于系统原因的事后分析:为什么引入了漏洞?为什么在审查中没有被捕获?什么流程变更可以防止未来出现这类漏洞?无指责的事后分析建立安全文化。指责驱动的事后分析摧毁它。

安全编码不是一次学习就终身受用的技能。威胁格局在演变。工具在演变。你自己的理解也在演变。每个开发者应该每季度投入时间了解新的攻击向量、新的防御技术和新工具。阅读 OWASP 速查表系列。关注安全研究人员在他们发布的平台上。针对你自己的应用运行渗透测试。目标不是实现完美的安全——那是不可能的。目标是使你的应用比下一个更难攻击,这样攻击者就会转向其他地方。

因为归根结底就是这样。安全不是关于构建一个牢不可破的系统。而是关于构建一个与替代方案相比不值得破坏的系统。你关闭的每个漏洞、你停止硬编码的每个密钥、你实现的每个限流措施,都让其他人成为更有吸引力的目标。不要做那个低垂的果实。