4. 安全编码规范:从源头堵住漏洞

安全编码,说白了就是在写代码的时候就想着「这行代码会不会被人利用」。我见过太多项目,功能跑得飞起,一上渗透测试就千疮百孔。为什么?因为开发时只关心「能不能跑」,没想过「会不会被黑」。

这一章,我们聊聊最常见的两类漏洞——注入和XSS,以及怎么用工具和清单把它们挡在门外。

4.1 常见安全编码错误:注入攻击

注入攻击的本质,是用户输入的数据被当成了代码执行。最典型的就是SQL注入。

核心原则:永远不要信任用户的输入。所有输入都是潜在的恶意载荷。

4.1.1 SQL注入:一个经典案例

看看这段代码,是不是很眼熟?

// 危险的写法:直接拼接SQL
String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);

如果用户输入 admin' OR '1'='1,SQL就变成了:

SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = ''

结果?直接登录成功。我在一个金融项目中遇到过类似问题,开发同学觉得「内部系统没人会搞」,结果测试环境被扫出十几个注入点。嗯,从那以后,我们强制所有SQL必须用参数化查询。

4.1.2 正确的做法:参数化查询

// 安全的写法:使用PreparedStatement
String query = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();

参数化查询会把输入当数据,而不是代码。这是最基础、最有效的防御手段。

4.1.3 其他注入类型

注入类型 攻击方式 防御措施
命令注入 在系统命令中拼接用户输入 避免直接调用系统命令,使用API替代
LDAP注入 篡改LDAP查询语句 使用转义库,过滤特殊字符
XML注入 在XML中插入恶意标签 使用白名单验证,禁用外部实体
NoSQL注入 利用MongoDB等数据库的查询语法 使用ORM框架,避免直接拼接

我的习惯:所有数据库操作统一走数据访问层,禁止在业务代码中直接写SQL。这样审计起来也方便。

4.2 跨站脚本攻击(XSS)

XSS是Web应用的头号公敌。攻击者把恶意脚本注入到页面中,偷Cookie、改页面、跳钓鱼网站,什么都能干。

4.2.1 XSS的三种类型

  • 反射型XSS:恶意脚本藏在URL参数里,服务端直接返回。用户点开链接就中招。
  • 存储型XSS:脚本存到数据库里,每次加载页面都执行。比如评论区、用户资料。
  • DOM型XSS:纯前端问题,恶意脚本通过DOM操作执行。不经过服务端。

我记得有一次代码审查,发现一个论坛的「用户签名」字段直接输出了HTML。攻击者可以在签名里写 <script>alert('xss')</script>,所有看帖的人都会弹窗。更可怕的是,如果换成窃取Cookie的脚本……

4.2.2 防御XSS的核心策略

输出编码是王道。简单说,就是把特殊字符转成HTML实体。

// 前端输出编码示例(JavaScript)
function escapeHtml(str) {
    return str
        .replace(/&/g, '&')
        .replace(/</g, '<')
        .replace(/>/g, '>')
        .replace(/"/g, '"')
        .replace(/'/g, ''');
}

// 使用
document.getElementById('username').innerHTML = escapeHtml(userInput);

注意:不要用 innerHTML 直接插入用户数据。优先使用 textContent 或 innerText,它们会自动转义。

4.2.3 Content Security Policy(CSP)

CSP是浏览器层面的安全策略。你可以告诉浏览器「只允许加载本域的脚本」,这样即使有XSS漏洞,攻击者的外部脚本也执行不了。

// HTTP响应头示例
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com; object-src 'none'

我个人建议,所有新项目都加上CSP。它不能完全替代输出编码,但多了一层保险。

4.3 静态代码扫描工具(SAST)集成

人工审查代码效率有限,尤其是大项目。SAST工具能自动扫描代码,发现潜在漏洞。

4.3.1 常用SAST工具对比

工具 支持语言 特点 适用场景
SonarQube Java, C#, Python, JS等 社区版免费,规则丰富 持续集成流水线
Checkmarx 20+种语言 精准度高,支持自定义规则 企业级安全审计
Fortify 广泛 与Micro Focus生态集成 大型组织合规
Semgrep 多语言 开源,规则可自定义 快速定制扫描

4.3.2 在CI/CD中集成SAST

我建议把SAST扫描作为流水线的一环。代码提交后自动触发,发现问题直接阻断合并。

# GitLab CI示例:集成SonarQube
stages:
  - test

sonarqube-check:
  stage: test
  script:
    - sonar-scanner
      -Dsonar.projectKey=my-project
      -Dsonar.sources=.
      -Dsonar.host.url=$SONAR_HOST_URL
      -Dsonar.login=$SONAR_TOKEN
  only:
    - merge_requests

避坑指南:SAST工具会有误报。我曾经遇到一个项目,SonarQube报了200多个漏洞,实际只有30个是真的。所以,不要盲目相信工具,要人工确认。

4.4 代码审查清单

代码审查是最后一道防线。我整理了一份清单,每次审查都对照着看。

4.4.1 通用安全审查项

  • 输入验证:所有外部输入是否做了白名单校验?长度、格式、类型都检查了吗?
  • 输出编码:用户数据输出到页面时,是否做了HTML/JS编码?
  • 认证与会话:密码是否哈希存储?会话ID是否随机?有没有设置HttpOnly和Secure标志?
  • 访问控制:敏感接口有没有做权限校验?是不是只检查了前端,没检查后端?
  • 错误处理:异常信息会不会暴露堆栈?生产环境是不是关闭了调试模式?

4.4.2 针对注入的审查清单

检查项 通过标准 常见违规
SQL查询 全部使用参数化查询或ORM 字符串拼接SQL
系统命令 禁止直接调用shell命令 使用Runtime.exec()
文件路径 使用白名单限制路径 直接拼接用户输入到路径
序列化 使用安全的序列化库 反序列化未校验的数据

4.4.3 针对XSS的审查清单

  • 模板引擎:是否开启了自动转义?比如Thymeleaf、React默认是转义的,但要注意 dangerouslySetInnerHTML。
  • 富文本:如果允许用户输入HTML,有没有用白名单过滤标签?比如只允许 <b>、<i>。
  • 事件处理器:有没有在 onclick、onerror 等属性中拼接用户数据?
  • URL:用户输入的链接有没有校验协议?防止 javascript: 伪协议。

我的经验:审查清单不是一次性的。每发现一个新漏洞,就加到清单里。这样团队的安全能力会越来越强。

4.5 本章知识体系

下面这张图,帮你理清安全编码的核心逻辑。

安全编码规范知识体系 安全编码规范知识体系 安全编码规范 常见安全编码错误 SAST工具集成 代码审查清单 注入攻击 XSS攻击 工具选型 CI/CD集成 通用审查项 专项审查项 SQL注入 命令注入 反射/存储/DOM SonarQube Checkmarx 输入/输出/认证 注入/XSS专项 核心原则:不信任用户输入 + 多层防御 工具辅助 + 人工审查 = 安全编码落地

安全编码不是一个人的事。从规范到工具,再到审查,每个环节都要到位。你想想看,如果开发阶段就堵住了漏洞,后面省下的可不止是修复成本,还有安全团队的口水仗。