f-string拼接sql极度危险,因用户输入如"admin' --"会闭合引号并注释后续校验,甚至执行任意python代码;必须用驱动规定的参数占位符(如%s、$1)并传元组参数,动态表名/字段名须白名单校验。

直接拼接字符串执行原生 SQL 会触发 SQL 注入,Tornado 自身不提供参数化查询封装,必须靠你自己把参数和语句彻底分离。
为什么 execute("SELECT * FROM user WHERE id = " + user_id) 是危险的
这种写法把用户输入直接混进 SQL 字符串里,攻击者传 "1 OR 1=1; DROP TABLE user;" 就能执行多条语句或删表。Tornado 的模板引擎、路由参数、JSON body、query string —— 所有入口都可能带恶意输入,没过滤就拼 SQL 等于开门揖盗。
- 即使只做
int(user_id)转换也不够:整数溢出、负号绕过、或字段本身是字符串类型(如username)时完全失效 -
escape()或html.escape()没用:那是防 XSS 的,对数据库层零防护 - Tornado 的
get_argument()返回的是原始字符串,不做任何 SQL 安全处理
安全执行原生 SQL 的唯一可行路径:用异步驱动 + 参数占位符
不能依赖 SQLAlchemy ORM 的同步 session,也不能用 run_on_executor 包裹字符串拼接逻辑——那只是把注入延迟到线程里执行,漏洞还在。
- MySQL 场景必须用
aiomysql,PostgreSQL 必须用asyncpg,它们原生支持参数化查询 - 占位符必须用驱动规定的格式:
aiomysql用%s,asyncpg用$1、$2,不能混用 - 参数必须以元组或列表传入
execute(sql, params),绝不能用 f-string 或.format() - 连接必须从连接池获取(
aiomysql.create_pool()),不能每次新建连接,否则压测时会耗尽 socket
示例(MySQL):
async def get_user(self, user_id):
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("SELECT * FROM user WHERE id = %s AND status = %s", (user_id, "active"))
return await cur.fetchone()
哪些地方容易漏掉参数化
开发者常在“看起来安全”的地方翻车,比如动态字段名、表名、ORDER BY 子句——这些无法用占位符,必须白名单校验。
-
ORDER BY {field}:field只能从["id", "name", "created_at"]中取,不能直接插变量 -
SELECT {fields} FROM ...:字段列表需拆成固定字符串拼接,或用预定义枚举映射 - 分表场景如
log_202610:表名后缀必须由服务端计算生成,禁止由 URL 参数直接拼入 - IN 查询如
WHERE id IN (1,2,3):需动态构造等长占位符串"%s, %s, %s",再传对应元组
别信“我只查一条,没必要”
SQL 注入不是按查询复杂度分级的,而是按输入是否可控。哪怕你只执行 SELECT version(),只要语句里掺了用户输入,就构成注入点。生产环境见过因一个调试用的 ?debug_sql=xxx 参数被利用,导致配置文件被读取的案例。参数化不是性能开销,是底线;没做,就不算“安全执行”。










