SQL 注入是一种通过表单输入字段或 URL 查询参数执行 SQL 命令的技术。这会导致未经授权地访问数据库(一种黑客攻击类型)。
如果 SQL 注入成功,未经授权的人员可能会从数据库表中读取、创建、更新甚至删除记录。这种技术主要被黑客、渗透测试人员、质量保证人员和安全研究人员使用(但不限于)。
使用多语句的 SQL 注入
假设我们的网站上有一个搜索表单,用于按产品 ID 搜索产品。搜索产品的 PHP 代码片段可能如下所示:
$prod_id = $_GET["prod_id"];
$sql = "SELECT * FROM Products WHERE product_id = " . $prod_id;
如果用户在表单中输入 20; DROP TABLE Products;
,那么 SQL 语句将变为:
SELECT * FROM Products WHERE product_id = 20; DROP TABLE Products;
现在,这条 SQL 语句会从数据库中删除 Products 表。这之所以可能,是因为大多数数据库系统可以同时执行多条语句。

使用“永远为真”条件的 SQL 注入
执行 SQL 注入的另一种方法是传递一个始终导致 TRUE
的条件,这样无论如何都会获取数据。
让我们看一下另一个 PHP 代码片段,我们的网站上有一个登录表单,需要通过提供凭据来获取用户。
$username = $_POST["username"];
$password = $_POST["password"];
$sql = "SELECT * FROM Users WHERE username = \"" . $username . "\" AND password = \"" . $password . "\"";
如果用户将用户名输入为 invalid_user" OR "1"="1
,将密码输入为 invalid_pass" OR "1"="1
,那么 SQL 语句将变为
SELECT * FROM Users WHERE username = "invalid_user" OR "1"="1" AND password = "invalid_pass" OR "1"="1"
由于 "1"="1"
始终为 TRUE
,无论用户输入什么 username
和 password
,SQL 都会从数据库中获取所有用户的数据。
如何保护 SQL 语句免受注入?
验证用户输入
在实际将用户输入数据发送到数据库之前,我们应该始终对其进行验证。一些最佳实践包括:去除空格、解析特殊字符、限制输入大小等。
例如,
$data = $_POST["name"];
$data = trim($data);
$data = stripslashes($data);
$data = htmlspecialchars($data);
此处,此 PHP 代码片段在一定程度上验证了输入数据。
使用 ORM
ORM(对象关系映射)是一个基本上将 SQL 语句解析为编程语言代码,反之亦然的工具。
如果我们使用 ORM,大部分情况下我们不必编写原始 SQL。由于 ORM 是根据良好实践和安全协议设计的,因此它们安全且易于使用。
例如,考虑下面的 SQL 代码
SELECT * FROM Users WHERE id = 5;
其在 Python 的 SQLAlchemy ORM 中的等效代码将是
select(Users).where(Users.id == 5)
使用预处理语句
保护 SQL 语句免受注入的另一种方法是使用预处理语句。
预处理语句基本上是带占位符的 SQL 语句。传递的参数只是替换占位符的位置。
例如,
$sql = "INSERT INTO Users (first_name, last_name, email) VALUES (?, ?, ?)";
mysqli_stmt_bind_param($sql, "sss", $first_name, $last_name, $email);
$first_name = "Harry";
$last_name = "Potter";
$email = "[email protected]";
mysqli_stmt_execute($stmt);
在这里,值只放置在 ?
占位符的位置,并且 SQL 语句的结构得以保留。
使用框架
此外,如果我们要构建一个实际世界的应用程序,最好始终使用框架(如 Django、Laravel、ASP.net 等),而不是从头开始编写代码。
这是因为这些框架默认处理 SQL 注入和许多其他常见问题。
结论
SQL 注入是攻击任何 Web 应用程序的一种非常常见的方式。
因此,如果我们在构建应用程序时使用原始 SQL 语句,我们必须彻底测试和验证它们。