在 symfony 中应始终用 querybuilder 构建查询以防止 sql 注入、保障缓存与类型安全;禁止字符串拼接 sql,动态字段名需白名单校验,原生查询必须参数绑定,动态条件用 expr() 组合,自定义 dql 函数须严格参数化。

用 QueryBuilder 替代字符串拼接
直接拼接 SQL 字符串在 Symfony 里几乎总是错的——不仅容易被注入,还会绕过 Doctrine 的缓存、类型转换和数据库抽象层。正确做法是用 QueryBuilder 构建查询,它会自动参数化所有用户输入。
常见错误现象:"SELECT * FROM user WHERE name = '" . $_GET['name'] . "'" 这类写法哪怕加了 htmlspecialchars 也拦不住 SQL 注入;EntityManager::createQuery() 手写 DQL 时若用 sprintf 或 str_replace 插入变量,同样危险。
- 始终用
$qb->setParameter('name', $input)绑定值,不要用$qb->where("u.name = '" . $input . "'") - 字段名、表名等非用户数据若需动态化(如多租户切换),必须白名单校验,不能直接拼进
FROM或JOIN -
addSelect()、addOrderBy()等方法支持表达式,但别往里面塞未过滤的字符串,比如$qb->addOrderBy($unsafeSortField)是高危操作
警惕 NativeQuery 和 executeStatement 的陷阱
Doctrine 允许执行原生 SQL,但一旦用了 createNativeQuery() 或 getConnection()->executeStatement(),就等于退出了安全护栏。这时候参数绑定规则没变,但开发者更容易松懈。
使用场景:调用存储过程、复杂窗口函数、或需要数据库特有语法(如 PostgreSQL 的 ILIKE)。
- 必须用
setParameter()或executeStatement($sql, [$param1, $param2]),绝不用sprintf($sql, $param) - 原生查询无法被 DQL 缓存,且返回的是数组而非实体,要手动映射;如果真需要实体,优先考虑
ResultSetMapping而不是自己解析字段 - MySQL 的
GROUP_CONCAT、PostgreSQL 的STRING_AGG等聚合函数,在原生查询里很常见,但它们的分隔符参数如果来自请求,一样得走参数绑定
动态条件别硬拼 WHERE,用 expr() 组合
搜索页、后台筛选这类功能常要根据参数存在与否增减 WHERE 条件。有人会写:$where .= " AND status = ?";,这是典型隐患。
QueryBuilder 的 expr() 就是干这个的,它返回可组合的表达式对象,不会产生字符串拼接逻辑。
- 用
$expr = $qb->expr(); $qb->where($expr->andX($expr->eq('u.status', ':status'), $expr->gt('u.createdAt', ':since'))),而不是拼字符串 - 空条件跳过即可:
if ($searchName) { $qb->andWhere($expr->like('u.name', ':name')); $qb->setParameter('name', "%$searchName%"); } - 注意
in()方法接受数组,但数组元素仍需绑定;别写$qb->where("u.id IN (" . implode(',', $ids) . ")"),应该用$qb->setParameter('ids', $ids)配合$expr->in('u.id', ':ids')
自定义 DQL 函数也要防注入
当项目注册了自定义 DQL 函数(比如 DATE_DIFF),并在 DQL 中调用它时,传入的参数如果未经处理,也可能成为注入入口——尤其该函数内部拼接了 SQL 片段。
性能影响不大,但安全链路会被打断:自定义函数绕过了 Doctrine 默认的参数绑定检查机制。
- 函数实现中,所有外部输入必须经
$queryBuilder->createNamedParameter($value)或显式绑定,不能直接插进 SQL 字符串 - 避免在函数里做字段名拼接,例如
"{$field} > {$value}";字段名应由上层白名单控制,值才走参数化 - 如果函数只是封装计算逻辑(如时间差天数),优先用数据库内置函数(
DATEDIFF、AGE),不自己拼
最易被忽略的一点:即使全程用 QueryBuilder,只要最后调用 $qb->getQuery()->getSQL() 拿出原始 SQL 去做日志、监控或二次加工,就可能意外暴露未绑定的占位符(如 ? 或 :param),或者诱使后续代码去“补全”它——这时候已经脱离了 Doctrine 的保护范围。











