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 本章知识体系
下面这张图,帮你理清安全编码的核心逻辑。
安全编码不是一个人的事。从规范到工具,再到审查,每个环节都要到位。你想想看,如果开发阶段就堵住了漏洞,后面省下的可不止是修复成本,还有安全团队的口水仗。