功能测试全过,AI 生成的登录接口仍在 cursor.execute 中拼接 SQL

作者:袖梨 2026-09-12

一个登录接口能正确返回 200 和 401,只能说明业务结果符合预期,却无法证明数据库调用方式安全。尤其在审查 AI 生成代码时,字符串拼接 SQL 很可能藏在全绿的功能测试之后。要识别这类问题,需要把检查点推进到 cursor.execute 的调用边界。

我做了个很小的对照实验:

两份登录实现,四个功能测试全部通过。

但其中一份代码把用户名和密码直接拼进 SQL,另一份使用参数化查询。

功能测试看到的是一样的结果:

正确密码 -> 200
错误密码 -> 401
functional_total=4/4

测试很绿,SQL 却不一定安全。

掘金-封面-契约测试-20260912.png

问题不在登录结果,而在 SQL 是怎么来的

容易出问题的实现:

def login(db, username: str, password: str):
    sql = (
        "SELECT id FROM users "
        f"WHERE username = '{username}' AND password = '{password}'"
    )
    db.execute(sql)
    row = db.fetchone()
    return ({"ok": True}, 200) if row else ({"ok": False}, 401)

参数化实现:

def login(db, username: str, password: str):
    db.execute(
        "SELECT id FROM users WHERE username = ? AND password = ?",
        (username, password),
    )
    row = db.fetchone()
    return ({"ok": True}, 200) if row else ({"ok": False}, 401)

如果测试只断言“正确密码能登录、错误密码返回 401”,两份实现都能通过。

原因是测试看的路径不同:

功能测试:
请求参数 -> 登录结果

SQL 边界:
请求参数 -> SQL 字符串或参数元组 -> cursor.execute

返回值正确,不能证明实际执行的 SQL 已经参数化。

补一条契约测试

不要只测登录响应,直接检查 cursor.execute 收到了什么。

def assert_parameterized(db, username, password):
    sql = db.sql or ""

    assert "?" in sql
    assert db.params == (username, password)
    assert username not in sql
    assert password not in sql

同一组断言,对两种实现的结果不同:

unsafe_login: FAIL
params=None
sql="SELECT id FROM users WHERE username = 'alice' AND password = 'correct-password'"

safe_login: PASS
params=('alice', 'correct-password')
sql='SELECT id FROM users WHERE username = ? AND password = ?'

这时,四个功能测试仍然全绿,但契约测试已经能把不安全实现拦下来。

扫描器负责找位置,契约测试负责守边界

再用 code-audit-cli 扫两份实现:

unsafe_login: findings=1
severity=high
pattern=sql-concat
line=6

safe_login: findings=0

扫描器给出一行值得人工检查的位置:

sql = (
    "SELECT id FROM users "
    f"WHERE username = '{username}' AND password = '{password}'"
)

契约测试则保证修复之后,这条路径不会悄悄退回字符串拼接。

两者是不同层级的检查:

  • 扫描器:在代码库里定位危险写法;
  • 人工确认:判断输入能不能到执行器;
  • 契约测试:锁定实际交给执行器的 SQL 和参数;
  • 功能测试:验证登录行为没有回归。

完整顺序是:

扫描定位
  -> 人工确认
  -> 参数化修复
  -> 契约测试
  -> 功能回归

这段代码最值得改的一句话

不要只问:

正确密码能不能登录?

还要问:

登录时,cursor.execute 实际收到的 SQL 和参数是什么?

前者证明功能正常,后者证明 SQL 边界没有失控。

AI 生成的代码也应该这样审。模型写出的登录逻辑可能完全正确,但执行路径仍然可能把输入拼进 SQL。功能测试全绿时,这个差异不会自动暴露。

实验边界:这里用的是可控的假数据库执行器,只验证“参数化查询”这一条契约,不代表覆盖了密码哈希、频率限制、账号枚举、日志和权限等全部安全问题。

公开规则和示例报告:

github.com/yuan1521913…

需要本地扫描器完整源码版:Gumroad

相关文章

精彩推荐