pdo防sql注入需三要素齐备:关闭模拟预处理(pdo::attr_emulate_prepares=>false)、用户数据全走占位符(命名式或问号式)、连接dsn中声明charset=utf8mb4;动态结构如表名列名须白名单校验。

用PDO预处理防止SQL注入,关键不是“写了prepare()就安全”,而是三个动作必须同时到位:关掉模拟预处理、所有用户数据走占位符、字符集在连接时声明。
必须关闭模拟预处理
PDO默认开启 PDO::ATTR_EMULATE_PREPARES = true,这会让PHP在本地把参数拼进SQL字符串,根本没交给数据库做真实预处理,等于白设。
- 连接时显式关闭:
PDO::ATTR_EMULATE_PREPARES => false - 错误写法:
$pdo = new PDO($dsn, $u, $p);(没配这一项) - 正确写法:
$pdo = new PDO(
"mysql:host=localhost;dbname=test;charset=utf8mb4",
$user, $pass,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false
]
);所有用户输入必须进占位符
哪怕只拼一个变量,整个防护就失效。占位符分两种,命名式(推荐)和问号式,都只传值,不参与SQL解析。
- ❌ 危险:用字符串拼接变量,即使调了prepare()
- ✅ 安全:用
:name或?,再通过execute()或bindValue()绑定 - LIKE查询的通配符(如
%)要在PHP里加好再传,不能塞进SQL模板右边
$keyword = "%{$_POST['kw']}%";
$stmt = $pdo->prepare("SELECT * FROM goods WHERE title LIKE ?");
$stmt->execute([$keyword]);字符集必须写在DSN里
宽字节注入可能绕过过滤,仅靠 SET NAMES utf8mb4 不够,必须在连接字符串中声明 charset=utf8mb4。
- ✅ 正确:
"mysql:host=localhost;dbname=test;charset=utf8mb4" - ❌ 不足:
"mysql:host=localhost;dbname=test"+ 后续执行SET NAMES
动态结构需白名单校验
表名、列名、排序方向(ASC/DESC)、分组字段等不能用占位符,必须提前验证是否属于允许范围。
- 例如排序方向:
$order = in_array($_GET['sort'], ['ASC', 'DESC']) ? $_GET['sort'] : 'ASC';- 再拼进SQL:
"ORDER BY created_at $order"











