最危险的SQL写法是直接拼接用户输入,如format或%格式化字符串,必须改用参数化查询。正确做法是使用text()配合命名占位符绑定参数,或优先采用ORM的filter()等安全方法,并辅以输入校验和权限最小化。

直接拼接字符串的 format 或 % 是最危险的写法
像 "station_name like '%{0}%'".format(name) 这种写法,本质等同于字符串拼接,数据库完全无法区分哪部分是语句、哪部分是数据。攻击者输入 test' OR '1'='1 就能闭合单引号并注入任意逻辑。
常见错误还包括:"water_id = {}".format(water_id)(整型也照常被注入)、text("select * from t where id = " + str(id))(绕过ORM校验)。
- 永远不要用
format、%、+拼接用户输入进 SQL 字符串 - 即使做了
int()转换,也要注意:若转换失败抛异常前已执行其他拼接,仍可能触发部分注入 -
text()包裹的原始 SQL 不自动做参数化,必须显式传参
Flask-SQLAlchemy 中正确使用 text() + 参数绑定
当必须写原生 SQL(比如复杂分页、视图查询),text() 是可选方案,但必须配合命名参数或位置参数绑定,不能靠字符串替换。
修复示例:
from sqlalchemy import text
<h1>❌ 错误:拼接</h1><p>filter_str = "station_name like '%{}%'".format(name)</p><h1>✅ 正确:用 :name 占位符,由 execute() 绑定</h1><p>sql_str = text("""
SELECT <em> FROM v_monitor_point
WHERE station_name LIKE :name_pattern
ORDER BY water_id
OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY
""")
result = db.session.execute(sql_str, {
"name_pattern": f"%{name}%",
"offset": (currentPage - 1) </em> limit,
"limit": limit
}).fetchall()- 占位符统一用
:param_name形式,避免混用?(SQLite 风格)和$1(PostgreSQL 风格),不同方言支持不一致 - LIKE 模糊匹配的
%必须在 Python 层拼好再传入,不能写在 SQL 字符串里——否则:name会被当作文本字面量,不带通配符 -
execute()的第二个参数必须是 dict(命名)或 tuple(位置),不能是展开的 kwargs
优先用 ORM 查询构造器替代原生 SQL
绝大多数条件查询不需要写 text()。SQLAlchemy 的 query.filter() 和 filter_by() 天然参数化,且支持链式组合,更安全也更易维护。
对应上例的修复方式:
# 假设模型类为 MonitorPoint
query = MonitorPoint.query.filter(MonitorPoint.water_name != '测试')
<p>if name:
query = query.filter(
(MonitorPoint.station_name.like(f'%{name}%')) |
(MonitorPoint.outfall_name.like(f'%{name}%'))
)
if code:
query = query.filter(
(MonitorPoint.station_code.like(f'%{code}%')) |
(MonitorPoint.outfall_code.like(f'%{code}%'))
)
if water_id:
query = query.filter(MonitorPoint.water_id == water_id)</p><p>monitor_point_list = query.offset((currentPage-1)*limit).limit(limit).all()
count = query.count()- 所有
like()、==、!=等操作符都会转成参数化语句,无需手动处理引号或转义 - 注意
|(或)和&(与)是 SQLAlchemy 的重载运算符,不是 Python 原生的or/and -
query.count()会生成SELECT COUNT(*),避免手写count_sql字符串
额外防护:输入清洗 + 权限最小化不能省
参数化查询是核心防线,但不是万能的。比如超长输入可能触发缓冲区溢出,或特殊字符干扰日志解析;数据库账号权限过大则放大漏洞后果。
- 对非必要字段做长度限制:
if len(name) > 100: return error - 拒绝明显恶意模式:
if re.search(r"(union|select|;|--|#)", name, re.I): abort(400)(仅作兜底,不能替代参数化) - 数据库连接账号只授予
SELECT权限(读接口场景),禁用DROP、INSERT、EXECUTE - 生产环境关闭
SQLALCHEMY_ECHO=True,避免把完整 SQL 打印到日志中泄露结构
真正容易被忽略的点是:参数化只保 SQL 语句安全,不保业务逻辑安全。比如 water_id 若是外键 ID,但没校验是否属于当前租户,照样导致越权访问——这已超出 SQL 注入范畴,得靠领域层鉴权兜住。

















