用好 doctrine 参数绑定和输出时强制转义可基本防御 sql 注入与 xss;参数绑定须覆盖 dql、query builder 和原生 sql 所有场景,不可拼接用户输入;非参数化部分如字段名、排序方向、in 列表等需白名单校验与类型强转;twig 默认转义防 html xss,但 js 上下文须用 |json_encode|raw,富文本需 htmlpurifier 过滤;数据库权限、错误提示、路由约束、验证层等配置层面同样关键。

用好 Doctrine 参数绑定 + 输出时强制转义,SQL 注入和 XSS 就基本拦住了。Symfony 本身不写 SQL、也不渲染 HTML,真正起作用的是你调用 setParameter() 的那一行,和调用 htmlspecialchars() 或 Twig 的 {{ variable|e }} 那一刻。
Doctrine 查询必须用参数绑定,别拼字符串
只要 SQL 模板里出现用户输入,就必须走参数化。DQL、Query Builder、原生 SQL 都支持,但写法稍有区别:
- DQL 中用
:name占位,再调setParameter('name', $input) - Query Builder 中写
$qb->where('u.email = :email'),再链式调->setParameter('email', $request->query->get('email')) - 原生 SQL 必须用
executeStatement($sql, $params)或executeQuery($sql)->setParameters($params);绝不能写executeStatement($sql . " WHERE id = " . $_GET['id']) - 常见错误:把路由参数直接插进 DQL 字符串,比如
"u.status = '" . $request->query->get('status') . "'"—— 这等于开门揖盗
无法参数化的语法要素必须白名单+强转
表名、字段名、ORDER BY 方向、IN 列表长度这些,数据库根本不接受占位符。绕过参数绑定不是“灵活”,是自毁防线:
- 字段名/排序字段:只允许来自预定义数组,例如
$allowedSortFields = ['name', 'email', 'createdAt'];,再用in_array($input, $allowedSortFields, true) - 排序方向:
$dir = strtolower($request->query->get('dir', 'asc')) === 'desc' ? 'DESC' : 'ASC'; -
IN列表:整型 ID 先array_map('intval', $ids),再交给 Doctrine 的setParameter('ids', $ids, Connection::PARAM_INT_ARRAY) - 禁用 PDO 模拟预处理:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false),但得确认 MySQLi 或现代 PDO 驱动真支持真实预处理
Twig 渲染默认转义,但 JS 上下文要手动处理
Twig 的 {{ variable }} 默认调用 htmlspecialchars(),防住大多数 HTML/XSS 场景。但一旦变量进到 JS 或属性值里,就得自己兜底:
- JS 内联脚本中输出变量:
var email = {{ email|json_encode|raw }};(|json_encode保证引号、反斜杠安全,|raw是因为 Twig 已转义过一次) - HTML 属性值中嵌入用户数据:
<div data-id="{{ id|e('html_attr') }}"></div> - 富文本内容不能靠
|e,得用HTMLPurifier库过滤,否则<img onerror="alert(1)">照样执行 - 千万别在 Twig 里写
{{ include('user_input.html.twig') }}—— 模板路径也可能是用户可控的,得白名单校验
权限与配置层面的隐形补丁
参数绑定和转义是主干,但数据库账号权限、错误提示、路由约束这些细节,常被忽略却一击致命:
- 数据库账号禁用
DROP、CREATE、LOAD_FILE权限,哪怕 ORM 不暴露这些操作,留着就是风险 - 生产环境关闭
display_errors,禁用 Doctrine 的 SQL Logger,避免泄露表结构或查询逻辑 - 路由参数加正则约束,比如
{id}强制数字,比在 Controller 里intval()更早拦截非法输入 - Form 或 Request 验证层用
@Assert\Email、@Assert\Length做语义校验,不是所有输入都该进数据库查一遍才判错
最危险的不是不会写 setParameter(),而是写了却在某个 IN 子句里手拼字符串,或在 JS 变量里裸插 Twig 变量。防线是分层的,但断点往往出现在你认为“这里应该没问题”的地方。











