广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何校验SQL输入合法性并检查JSON/XML数据结构?

后端接口接收的用户输入通常包含两类风险:一是拼进SQL语句造成注入,二是JSON、XML等结构化数据格式与预期不一致导致解析异常。如果只依赖前端校验,攻击者完全可以直接构造请求体绕过。因此服务端需要从参数合法性、数据结构完整性和解析安全性三个层面设计防线。

如何校验SQL输入合法性并检查JSON/XML数据结构?

一、SQL输入合法性:参数化与白名单双保险

SQL注入的本质是用户输入被解释成了SQL语法的一部分。沿用字符串拼接的方式,即使业务代码看起来正常,只要输入中包含单引号、注释符或者恒等条件,原始语句就可能被改写。先看一个反面例子:

// 危险写法:直接拼接
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);

如果 name 的值是 ' OR '1'='1 之类的内容,查询条件就会被绕过。正确的做法是使用预编译语句,让数据库驱动把参数当作数据处理,而不是当作SQL语法执行。改写后的代码如下:

String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, name);
ResultSet rs = ps.executeQuery();

这里占位符 ? 只代表一个值,数据库在解析SQL时不会把它和关键字、运算符混在一起。对于绝大多数业务查询,参数化就能挡住注入攻击。不过它并不覆盖所有情况,例如动态表名、动态列名、排序方向以及 LIMIT 等标识符位置,数据库不允许使用占位符。此时必须在代码层做白名单校验,而不是简单过滤几个关键字。

动态标识符校验通常采用白名单策略,具体步骤为:

  • 定义允许的字段集合,例如 id、username、create_time;
  • 排序方向只允许 ASC 和 DESC;
  • 无法匹配时直接抛出参数错误,而不是拼进SQL继续执行。

可以用一个简单方法完成校验:

private static final Set<String> ALLOWED_COLUMNS = Set.of("id", "username", "create_time");
private static final Set<String> ALLOWED_DIRECTIONS = Set.of("ASC", "DESC");
public List<User> queryByOrder(String orderBy, String direction) {
    if (orderBy == null || !ALLOWED_COLUMNS.contains(orderBy)) {
        throw new IllegalArgumentException("非法排序字段");
    }
    String dir = direction == null ? null : direction.toUpperCase();
    if (dir == null || !ALLOWED_DIRECTIONS.contains(dir)) {
        throw new IllegalArgumentException("非法排序方向");
    }
    String sql = "SELECT * FROM users ORDER BY " + orderBy + " " + dir;
    return jdbcTemplate.query(sql, new UserRowMapper());
}

除了应用层校验,数据库账号权限也要收敛。应用连接不要使用管理员账号,不开启多语句执行功能,这样即使某处校验被绕过,攻击者能做的事情也会受到限制。把参数化查询、白名单校验和最小权限三者叠加,SQL输入的风险才能降到可接受范围。

二、JSON数据校验:用Schema锁定结构

JSON解析函数只能证明一段文本是合法JSON,却不代表它的字段类型、必填项和嵌套结构符合接口约定。例如客户端漏传 age,或者把数组 tags 传成字符串,这些错误如果不在入口拦截,就会一路传导到业务代码里,最终表现为空指针、类型转换异常甚至脏数据落库。因此对JSON做结构校验,核心是明确声明期望的数据形状。

JSON Schema 是目前比较通用的方案,它本身也是JSON文档,通过 type、properties、required、additionalProperties 等关键字描述约束。以下是一个Python示例:

import jsonschema
schema = {
    "type": "object",
    "properties": {
        "username": {"type": "string", "minLength": 3, "maxLength": 20},
        "age": {"type": "integer", "minimum": 1, "maximum": 120},
        "tags": {"type": "array", "items": {"type": "string"}, "maxItems": 10}
    },
    "required": ["username", "age"],
    "additionalProperties": False
}
data = {"username": "alice", "age": 25, "tags": ["admin", "dev"]}
try:
    jsonschema.validate(instance=data, schema=schema)
except jsonschema.ValidationError as e:
    print(e.message)

这段Schema要求根节点必须是对象,username 和 age 为必填项,不允许出现额外字段,username 长度限制在3到20之间,tags 最多10个字符串元素。校验失败时会抛出 ValidationError,后端可以据此返回明确的字段错误。

如果接口使用Go、Java或TypeScript等语言,结构体绑定能完成一部分基础校验,但对长度、枚举、正则格式、数组最大长度等约束仍然需要额外判断。JSON Schema 的优势在于这些规则可以集中管理,并且前后端可以共用同一份Schema文档。实现时需要注意两点:一是解析前先限制请求体大小,避免超大JSON消耗内存;二是把Schema对象缓存到进程启动阶段,不要每次请求都重新编译。

同时,JSON的嵌套深度也应当设置上限。不少解析器默认允许深层嵌套,攻击者可以构造几千层对象让递归解析栈溢出。可以在反向代理层限制 Content-Length,或者在解析后遍历结构检查深度。动态结构的JSON至少也要校验顶层字段和公共字段,不能因为字段不固定就完全放弃校验。

三、XML结构校验与XXE防护

XML解析比JSON更容易踩到安全坑,主要原因在于DTD和外部实体。一个看似正常的XML文档,可能通过 <!DOCTYPE> 声明引入外部实体,让解析器去读取服务器本地文件。例如经典的XXE攻击会构造实体 <!ENTITY xxe SYSTEM "file:///etc/passwd">,然后在正文中引用该实体,解析结果就会把文件内容暴露出来。对于接收XML的接口,光做结构校验还不够,必须先关掉危险特性。

下面展示一个更直观的XML片段:

<user>
  <username>alice</username>
  <age>25</age>
</user>

如果这个XML不是由可信客户端生成,解析器可能尝试加载DTD。Java中可以使用 DocumentBuilderFactory 进行安全配置:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));

上述配置会禁用DOCTYPE声明和外部实体,从根上规避XXE。对于必须保留DTD的场景,也要设置实体扩展限制,防止实体爆炸式递归展开导致内存耗尽,也就是常说的 Billion Laughs 攻击。

在结构校验方面,XML Schema(XSD)可以定义元素顺序、出现次数、数据类型和枚举。Python 的 lxml 库提供了较完整的XSD支持:

from lxml import etree
with open("schema.xsd", "rb") as f:
    schema_root = etree.XML(f.read())
schema = etree.XMLSchema(schema_root)
xml_doc = etree.fromstring(xml_bytes)
if not schema.validate(xml_doc):
    for error in schema.error_log:
        print(error.message)

实际项目中,可以把XSD文件放在资源目录下,应用启动时加载并缓存 XMLSchema 对象。每次请求只调用 validate,避免重复解析XSD。命名空间较多的XML还要注意元素前缀,XSD中的 targetNamespace 需要与XML内容匹配,否则会因为命名空间不一致误报校验失败。

如果接口根本不涉及DTD或外部实体,最简单的策略仍然是禁止DOCTYPE。对XML做校验时,先做安全配置,再进行XSD校验,顺序不要反。

四、构建统一校验层与错误响应

把SQL、JSON、XML的校验分散在各个Service里,会出现重复代码和遗漏。更合适的做法是将输入验证集中到中间件或AOP层,在进入业务处理之前完成格式、类型、长度和Schema校验。对于校验失败,统一返回400状态码和结构化错误信息,避免把数据库异常或堆栈直接抛给调用方。

以下是一个Node.js Express中间件的简化示例,它先对请求体做基础检查,再放行到后续逻辑:

function validatePayload(req, res, next) {
    const errors = [];
    if (!req.body.username || typeof req.body.username !== "string") {
        errors.push("username 必须为字符串");
    }
    if (!Number.isInteger(req.body.age) || req.body.age < 1 || req.body.age > 120) {
        errors.push("age 必须为1到120之间的整数");
    }
    if (errors.length > 0) {
        return res.status(400).json({ code: "INVALID_INPUT", errors });
    }
    next();
}
app.post("/users", validatePayload, (req, res) => {
    // 通过校验后执行数据库操作
    res.json({ success: true });
});

错误响应可以设计成固定结构,例如 code 表示错误类别,errors 数组列出字段级原因。SQL注入拦截和Schema校验失败可以共用这份格式。注意不要把SQL细节、表结构或XSD路径返回给客户端,攻击者会利用这些信息进一步探测。

校验顺序建议按成本从低到高执行:先做基础长度和类型检查,再做JSON Schema或XSD结构校验,最后才执行业务规则和数据库操作。Schema对象缓存后,单次校验通常只是微秒级开销,不会成为接口瓶颈。同一套Schema还可以在接口文档、Mock服务和自动化测试中复用,减少前后端理解偏差。

无论接口多简单,服务端都不能假设客户端会乖乖按约定传参。SQL参数化、JSON Schema、XML安全解析和统一错误响应,这几件事放在一起,才能形成完整的前置防线。

广告位 · 文章底部(后台-模块管理替换此区域)