PHP 程序如何防范 SQL 注入

Laravel

问题

Web 应用程序中最常见的安全威胁之一是 SQL 注入。这个问题持续霸榜在 OWASP 应用程序安全风险列表 的首位。然而不知为何,许多开发人员甚至不知道它是什么

SQL 注入是指恶意 SQL 查询被注入到应用程序运行的合法查询中,注入通常是从恶意用户通过用户界面上可输入的位置(如表单、输入框等)开始的。当应用程序没有保护数据库免受原始用户输入(在如此多 PHP 应用程序中很常见)并且巧妙形成的输入诱使数据库运行恶意查询时,就会发生这种情况。

一个简单的例子

想象一下,在存有用户配置信息的应用程序中的这个查询,其中 $_GET['userId'] 来自请求查询提交的字符串,用于识别当前正在查看的配置信息对应用户。

$query = 'SELECT * FROM Users WHERE id=' + $_GET['userId'];

这个查询将返回我们正在查看的用户数据。

但是,如果查看者将查询字符串更改为 profile.php?userId=1%20OR%201=1 呢?

根据上面的代码,进入数据库的查询看起来像这样。

SELECT * FROM Users WHERE id=1 OR 1=1

这是一个有效的查询,如果你的应用程序只是按原样运行它,不采取任何措施来保护数据库免受用户输入的影响,那么该查询将为用户表中的每条记录评估为「ture」。 这将返回表中每条记录的所有数据。 吗可能会泄露你所有 的用户的详细信息。

然而,你当然应该 验证所有用户输入 进入您的应用程序,但这是完全不同的责任。这更多的是拒绝不合理的恶意数据,而不是保护数据库。

与有关该话题的大量过时教程相反,清理用户输入的数据并不能阻断这个问题。正如 Paragon Initiative 的 SQL 注入指南 所说:

虽然可能通过在将传入数据流发送到数据库驱动程序之前重写传入数据流来防止攻击,但它[充满危险的细微差别](kraft.im/2015/05/how-emoji-saved- your-sites-hide) 和 模糊边缘案例。 (强烈推荐上一句中的两个链接。)

除非你想花时间研究并完全掌握你的应用程序使用或接受的每种 Unicode 格式,否则最好不要尝试清理你的输入。准备好的语句在防止 SQL 注入方面比转义字符串更有效。

此外,更改传入数据流可能会导致数据损坏,尤其是在处理原始二进制数据(例如图像或加密消息)时。

预先设计好的 SQL 语句可以更容易且有效保证防止 SQL 注入。

(你可能会在其他地方看到这种技术被称为「参数化查询」或「参数绑定」。实际上,这些概念是等价的。)

解决方案

在使用 MySQL 数据库的 PHP 应用程序中防止 SQL 注入实际上并不难。

原生预设好的语句保证可以防止 SQL 注入攻击。

想象一下这样的预设语句:你「预设」了一个带有占位符的查询 SQL 模板,占位符代表了查询中的可变数据。然后分别为这些占位符发送数据并告诉数据库执行查询。

上面重写为预设好的语句的代码如下所示。

$statement = $pdo->prepare('SELECT * FROM Users WHERE id=?');

$statement->execute([$_GET['userId']]);
$user = $statement->fetch(PDO::FETCH_ASSOC);

使用原生预设好的语句,这个交互过程确实发生在 2 个单独的请求中。因为语句和数据是分开发送的——字面意思是作为单独的数据包发送到数据库服务器——巧妙形成的数据无法改变 SQL 查询的结构。

如果你启用了 SQL 方言模拟 ,则准备好的语句和数据不会单独发送,因此你应该禁用它。SQL 方言模拟的预设语句的构造方式不同(由 PHP 而不是由数据库服务器),并且不能完全保护你免受 SQL 注入。

重点: 从来不要允许用户直接在你声明的查询模板中输入,例如让用户输入指示正在查询哪个表或列。这样做仍然会让你容易受到攻击。 这种情况下,你可以 考虑使用白名单

使用PDO连接到数据库,明确指名连接字符集,并禁用本地模拟预处理SQL。同时,保险和安全起见,强烈推荐设置开启PDO的异常开关

$pdo = new PDO(
    'mysql:host=localhost;dbname=db_name;charset=utf8mb4',
    'db_user',
    'db_pass'
);

// 关闭本地模拟预处理SQL
// 特殊场景用到 如当连接PostgreSQL数据库时,如果开启本地模拟预处理像类似SQL `select * from table where column ??  1` 将无法正确执行并直接抛出异常
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

// 设置开启PDO的异常抛出开关
// 当发生异常时抛出异常而非返回false
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

⚠️ 注意:必须在PDO的DSN参数中设置连接字符集(而不是使用SET NAMES< AAAA>)以防止这种潜在漏洞

❓ 如需了解更多相关细节,请阅读本篇文章中多次提到的参考文章《防 SQL 注入权威指南》 和 《详尽的 PDO 解释器》.

使用PDO连接到数据库,明确指名连接字符集,并禁用本地模拟预处理SQL。同时,保险和安全起见,强烈推荐设置开启PDO的异常开关

$pdo = new PDO(
    'mysql:host=localhost;dbname=db_name;charset=utf8mb4',
    'db_user',
    'db_pass'
);

// 关闭本地模拟预处理SQL
// 特殊场景用到 如当连接PostgreSQL数据库时,如果开启本地模拟预处理像类似SQL `select * from table where column ??  1` 将无法正确执行并直接抛出异常
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

// 设置开启PDO的异常抛出开关
// 当发生异常时抛出异常而非返回false
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

⚠️ 注意:必须在PDO的DSN参数中设置连接字符集(而不是使用SET NAMES)以防止这种潜在漏洞

❓ 如需了解更多相关细节,请阅读本篇文章中多次提到的参考文章definitive guide on preventing SQL injection 和 this exhaustive PDO explainer.

你可能已经受到保护

如果你使用的查询构建器或 ORM 总是 使用原生预设语句并在 PDO 的 DSN 参数中设置连接字符集,那么你已经受到保护。但是,在你进行一些研究以确保你最喜欢的数据库抽象模型使用原生准备好的语句之前,请不要松一口气。

尽管如此,熟悉这些技术仍然很重要。在某些时候,你会遇到需要直接查询数据库的情况。或者,你会发现自己正在处理一个没有可以依赖的查询构建器或 ORM 保护的遗留应用程序。

确保你的应用程序免受最普遍的 Web 应用程序威胁之一破坏,这始终是你不可推卸的责任。

sql
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://kevinsmith.io/protect-your-php-a...

译文地址:https://learnku.com/laravel/t/65661

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 3

到底PDO的预处理能不能100%防止注入?

2年前 评论
cevin 2年前

干嘛要写原生语句,我一直用的laravel 查询构造器

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!