=========================
SQL 注入(SQL Injection)长期位居 OWASP Top 10 榜首,是 Web 安全领域最具破坏力的漏洞类型之一。攻击者通过在用户输入中嵌入恶意 SQL 片段,可以绕过认证、读取敏感数据、篡改记录,甚至直接控制数据库服务器。对于使用 MySQL 的开发者来说,掌握正确的防注入写法,是保障应用安全的基本功。
本文将从代码层面出发,系统梳理 MySQL 环境下防止 SQL 注入的几种主流写法,并指出常见误区。
一、SQL 注入的本质
SQL 注入的根源在于:用户输入被当作 SQL 代码执行。例如下面这段 PHP 代码:
$sql = "SELECT * FROM users WHERE username = '$_GET[user]' AND password = '$_GET[pass]'";
当攻击者提交 user=admin' -- 时,SQL 语句被拼接为:
SELECT * FROM users WHERE username = 'admin' -- ' AND password = '...'
-- 之后的内容被注释掉,密码验证形同虚设。问题的核心不是 MySQL 本身,而是拼接字符串构建 SQL 这一危险做法。
二、写法一:预处理语句(Prepared Statements)
预处理语句是目前最推荐的防注入方案,也是 OWASP 官方首推的做法。其原理是:SQL 模板先发送给 MySQL 编译,参数值随后单独传输,数据库引擎不会将参数值解析为 SQL 语法。
PDO 写法(PHP)
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :user AND password = :pass');
$stmt->execute([':user' => $username, ':pass' => $password]);
$user = $stmt->fetch();
MySQLi 写法(PHP)
$stmt = $mysqli->prepare('SELECT * FROM users WHERE username = ? AND password = ?');
$stmt->bind_param('ss', $username, $password);
$stmt->execute();
Java JDBC 写法
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE username = ? AND password = ?");
ps.setString(1, username);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();
Python(PyMySQL / mysql-connector)
cursor.execute(
"SELECT * FROM users WHERE username = %s AND password = %s",
(username, password)
)
注意:Python 驱动中占位符是 %s,不要用 Python 的字符串格式化提前替换,否则预处理失效。
关键点:参数值必须通过 bind / execute 传入,绝不能先拼接再 prepare。
三、写法二:参数化查询 + 白名单校验
预处理语句解决的是“值”层面的注入,但有些场景参数无法占位,例如表名、列名、排序方向(ORDER BY ASC/DESC)。这些位置不能使用占位符,必须使用白名单校验:
$allowedOrder = ['id', 'created_at', 'username'];
$order = in_array($_GET['order'], $allowedOrder, true) ? $_GET['order'] : 'id';
$direction = strtoupper($_GET['dir']) === 'DESC' ? 'DESC' : 'ASC';
$sql = "SELECT * FROM users ORDER BY {$order} {$direction}";
白名单的本质是:只允许已知安全的值通过,而不是试图过滤所有恶意输入。
四、写法三:输入类型强制转换
对于明确为整数、布尔值的参数,强制类型转换可以彻底消除注入风险:
$id = (int) $_GET['id'];
$sql = "SELECT * FROM articles WHERE id = $id";
int id = Integer.parseInt(request.getParameter("id"));
user_id = int(request.args.get("id"))
只要类型转换成功,攻击者就无法注入任何 SQL 片段。但要注意:类型转换只适用于数字型参数,字符串型参数仍需预处理。
五、写法四:ORM 框架的正确使用
现代 ORM(如 Laravel Eloquent、Django ORM、Hibernate、MyBatis)底层大多使用预处理语句,只要按规范使用即可自动防注入:
// Laravel
User::where('username', $username)->where('password', $password)->first();
# Django
User.objects.filter(username=username, password=password)
但 ORM 也有陷阱:
- Laravel 中使用
DB::raw()拼接用户输入会重新引入注入风险; - Django 中使用
.extra()或RawSQL需格外小心; - MyBatis 中
${}是字符串拼接,#{}才是参数化,务必使用#{}。
六、写法五:存储过程(谨慎使用)
存储过程在数据库端预编译,理论上可以防注入,但前提是存储过程内部也不拼接 SQL:
CREATE PROCEDURE GetUser(IN uname VARCHAR(50))
BEGIN
SELECT * FROM users WHERE username = uname;
END
如果存储过程内部使用 CONCAT 动态拼接,则同样存在注入风险。因此存储过程并非“银弹”,使用门槛较高。
七、常见误区与错误做法
- 依赖
addslashes()或mysql_real_escape_string():在字符集不匹配(如 GBK)时可能被宽字节注入绕过,且无法覆盖数字型参数。 - 使用黑名单过滤
union、select关键字:攻击者可通过大小写、注释、编码等方式绕过,防不胜防。 - 只在前端做校验:前端 JS 校验可被轻易绕过,服务端必须重新校验。
- 认为 ORM 绝对安全:一旦使用原生 SQL 拼接,ORM 的保护即失效。
- 错误信息直接回显:泄露表结构、字段名,为攻击者提供便利。
八、纵深防御建议
除了上述写法,还应配合以下措施:
- 最小权限原则:Web 应用连接数据库的账号只授予必要权限,禁用
DROP、FILE等高危权限; - 关闭错误回显:生产环境统一返回通用错误页面,详细错误写入日志;
- WAF 辅助:部署 Web 应用防火墙,拦截常见注入特征;
- 定期扫描:使用 SQLMap、AWVS 等工具进行安全测试;
- 日志审计:记录异常 SQL 行为,及时发现攻击。
结语
防止 SQL 注入没有“一招鲜”,但有一条铁律:永远不要信任用户输入,永远不要拼接 SQL。预处理语句是首选方案,白名单和类型转换是必要补充,ORM 需规范使用,存储过程要谨慎评估。将这些写法融入日常编码习惯,配合最小权限、错误处理和日志审计,才能构建真正稳固的 MySQL 应用安全防线。
安全不是一次性的工作,而是持续的过程。建议开发者将本文提到的几种写法纳入团队代码规范,并在 Code Review 中重点检查 SQL 拼接行为,从源头杜绝注入漏洞。
未经允许不得转载:任鹏个人博客 » MySQL 防止 SQL 注入的几种写法


朋友圈点赞图在线生成源码