2019 PHP安全指南
2019 年,大多数的科技工作者 — 尤其是 Web 开发者 — 必须摈弃掉关于开发安全 PHP 应用的老一套。这对那些不相信能够开发出安全的 PHP 应用的人来说尤其重要.
这篇指南应该作为 PHP: The Right Way 这本电子书强调安全部分的补充,而不是作为代码风格一样的普通主题.
PHP 版本
长话短说::除非你没有办法, 2018 年你在最好使用 PHP 7.2 , 同时在 2019 年早些时候计划切换到 7.3。
PHP 7.2 已经于 2017 年 11 月 30 号发布.
在撰写本文时,只有 PHP 7.1 和 7.2 会得到了 PHP 语言开发人员的积极支持,而 PHP 5.6 和 7.0 只会在大约一年的时间内获得安全补丁。
虽然一些操作系统为不受支持的 PHP 版本提供长期支持,但是这种做法普遍被认为是有害的。特别在于,他们会在不升级版本号的情况下提供安全补丁,这个坏习惯会使得仅通过 PHP 版本来判断系统的安全性变得非常困难。
综上,无论其他供应商做出什么样的承诺,只要你能够做到,都应该在任何时间内只运行获得积极支持的 PHP 版本。这样,即使使用了一段时间的安全版本,持续不断的升级工作也会让你的生活免于不愉快的意外。
依赖管理
简而言之:使用 Composer .
Composer 是对于 PHP 生态系统最先进的依赖管理方案, 我们强烈推荐的 PHP: The Right Way 里有专门一整节用于如何开始使用 Composer .
如果你不使用 Composer 来管理你的依赖项,你最终会 (希望延迟但可能事情很快发生) 出现这样一种情况,你所依赖的库变得过时,旧版本的漏洞被黑客利用.
重点: 始终记得在开发软件时更新依赖项 . 幸运的是,这是一条命令:
composer update
如果你正在做一些特别需要使用 PHP 扩展(用 C 语言编写)的东西,你就不能用 Composer 安装它们。您还需要 PECL.
推荐的包
无论您正在构建什么,您几乎肯定会从这些依赖项中获益。这是大多数 PHP 开发人员推荐的(比如:PHPUnit、PHP-CS-fixer)。
roave/security-advisories
Roave 的安全报告 使用了名叫 Friends of PHP 的包来保证你的项目不会依赖任何已知的非健壮的包。
composer require roave/security-advisories:dev-master
或者,你可以 上传你的 composer.lock
文件到 Sensio Labs 作为常规自动漏洞评估工作的一部分,它会把所有使用的过期的包告知你。
vimeo/psalm
Psalm 是一个静态分析工具,可以帮助识别你代码中的 bug。虽然还有其他很好的静态分析工具(例如: Phan and PHPStan ,它们都很好),如果您发现自己需要支持 php 5,没问题,psalm 是支持 php 5.4 + 的。
Psalm 是很容易上手的:
# 目前还没有Version 1 ,但是让我们拭目以待:
composer require --dev vimeo/psalm:^0#
仅需这样操作:
vendor/bin/psalm --init#
一如惯例的操作:
vendor/bin/psalm
如果你是第一次在现有的代码库上运行,可能会看到很多红色提示。这是正常的,除非你创造了一个如同 Wordpress 一样强大的应用程序,否则不太可能使全部测试通过以满足符合 Herculean 的要求。
无论您使用哪种静态分析工具,我们建议您将运用到持续集成工作流(如果适用),以便在每次代码更改后运行它。
HTTPS 和浏览器安全
简而言之: HTTPS, which should be tested, and security headers.
在 2018 年,通过不安全的 HTTP 访问网站将不再被接受。幸运的是,得益于 ACME 协议和 Let's Encrypt certificate authority 的存在,我们可以免费获取 TLS 证书并自动更新它们。
将 Acme 集成到 Web 服务器中简直就是小菜一碟。
Caddy: 自动生成.
Apache: 不久将以 mod_-md. 的形式提供, 这里有高质量的教程 在互联网上提供。
Nginx: 相对简单.
你也许会想,“好吧,我有一个 TLS 证书。现在,我必须花几个小时来处理配置,然后才能保证它的安全和快速。”
Nope! Mozilla 支持. 您可以使用配置生成器根据预期的访问群体构建推荐的密码套件 。
HTTPS (HTTP over TLS) 是绝对不可协商如果您希望您的网站是安全的。使用 HTTPS 可以立即消除对用户的几类攻击(中间建注入、窃听、重复攻击和多种形式的会话操作,否则将允许用户模拟)。
安全 Headers
虽然将 HTTPS 连接到服务器上确实为用户增加了安全性和性能优势,但你可以通过利用其他浏览器安全功能更进一步的扩大这些优势。其中大多数都涉及在内容里发送 HTTP 响应头。
Content-Security-Policy
你需要这个 header,因为它可以对浏览器允许加载的内部和外部资源进行精度控制,从而为跨站点脚本漏洞提供强大的防御层。
请参阅 CSP-Builder,以便快速轻松地部署 / 管理内容安全策略。
要进行更深入的分析,Scott Helme 的 Content-Security-Policy 标题简介是一个很好的起点。
Expect-CT
你需要这个 header,因为它通过强制将不良居心的人的证书的证据发布到可公开验证的数据结构,从而为恶意 / 受损的证书颁发机构添加了一层保护。 了解更多关于 “Expect-CT” 的信息。
首先将这个 header 设置为
enforce,max-age = 30
,并增加max-age
,如此操作让服务更稳定。Referrer-Policy
你需要这个 header,因为它允许您控制是否将有关用户行为的信息泄露给第三方。
再次深入了解 Scott Helme 深入探讨了
Referrer-Policy
headers将 header 设置为 “same-origin” 或 “no-referrer”,除非您有理由允许更宽松的设置。
Strict-Transport-Security
你需要这个 header,因为它告诉浏览器通过 HTTPS 强制所有将来的请求到同一个来源而不是不安全的 HTTP。
首次部署时将其设置为 “max-age = 30”,然后当您确信没有任何内容会中断时,将此值增加到某个较大的值(例如 “31536000”)。
X-Content-Type-Options
你需要这个 header,因为 MIME 类型混淆可能导致不可预测的结果,包括允许 XSS 漏洞的奇怪边缘情况。最好伴随标准的
Content-Type
标题。设置为
nosniff
,除非您需要默认行为(例如,用于文件下载)。X-Frame-Options
你需要这个 header,因为它可以防止点击劫持。
设置为
DENY
(或SAMEORIGIN
,但仅当你使用<frame>
元素时)X-XSS-Protection
- 你需要这个 header,因为它启用了默认情况下未启用的某些浏览器 Anti-XSS 功能。
- 设为1; mode=block
同样,如果你使用 PHP 的内置会话管理功能(推荐使用),你可能想要调用 session_start()
,如下所示:
session_start([
'cookie_httponly' => true,
'cookie_secure' => true]);
这会强制你的应用在发送会话标识符 cookie 时使用仅 HTTP 和安全标记,阻止成功的 XSS 攻击窃取用户的 cookie 并强制它们分别仅通过 HTTPS 发送。我们之前已经在 2015 年的博文中介绍了安全 PHP 会话
子资源一致性攻击(Subresource Integrity)
在将来的某个时候,你可能会使用 CDN 将常用的 Javascript / CSS 框架和库放到一个集中的地方。
负责安全的工程师对这种操作表示有很明显的安全隐患:那就是,许多网站使用 CDN 来提供内容,那么攻击 CDN 并替换内容注入这些代码到成千上万数千的网站。
输入 subresource integrity.
Subresource integrity (SRI) 允许你固定你希望 CDN 提供的文件内容的哈希值。当前实现的 SRI 仅允许使用安全加密散列函数,这意味着攻击者生成与原始文件产生相同散列的相同资源的恶意版本是不可行的。
真实的案例: Bootstrap v4-alpha 在他们的 CDN 示例片段中使用 SRI.
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ"
crossorigin="anonymous"/><script
src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js"
integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn"
crossorigin="anonymous"></script>
文档关联
Web 开发人员经常在超链接上设置 target
属性(例如:target="_blank"
)实现在新窗口中打开链接。但是,如果您没有传递 ' rel="noopener" 标记,则可能允许目标页面控制原始页面。
不要这样做
<a href="http://example.com" target="_blank">Click here</a>
这让example.com
可以控制当前的网页。
替换成这样
<a href="https://example.com" target="_blank" rel="noopener noreferrer">Click here</a>
这将在一个新窗口中打开 example.com
,但不会将当前窗口的控制权交给可能怀有恶意的第三方。
延伸阅读。
开发安全的 PHP 软件
如果应用程序安全性是一个新主题,请从应用安全性详解开始.
许多安全领域的专家都会向开发人员提供诸如 OWASP Top 10 之类的资源.
但是,大多数常见漏洞可以归类为同一类的安全问题(代码 / 数据没有完全分离,逻辑不健全,操作环境不安全或加密协议缺陷)。
给新手们说明安全问题是一个很简单的,容易实现的事情,但是如何解决这些安全问题将是更长远的安全工程。
因此, 我们只推荐类前 10 名或前 25 名安全检查清单.
数据库交互
深入了解: PHP 防止 SQL 注入 https://paragonie.com/blog/2015/05/preventing-sql-injection-in-php-applications-easy-and-definitive-guide
如果您自己编写 SQL 查询,请确保您使用的是预备表达式,并且将网络或文件系统提供的任何信息都作为参数传递,而不是拼接查询字符串。此外,确保你没有使用模拟的预备表达式.
为达到最佳效果,使用 EasyDB.
* 不要这样做:
/* 不安全代码:
*/$query = $pdo->query("SELECT * FROM users WHERE username = '" . $_GET['username'] . "'");
替换成这样:
/* 防止SQL注入: */
$results = $easydb->row("SELECT * FROM users WHERE username = ?", $_GET['username']);
还有其他一些数据库抽象层提供了同等的安全性(EasyDB 实际上在底层使用 PDO,但是为了防止安全隐患,它特意禁用准备好的语句模拟,而使用实际的准备好的语句)。只要用户输入不能影响查询的结构,您就是安全的。(这包括存储过程。)
文件上传
深入了解: 如何安全地允许用户上传文件 https://paragonie.com/blog/2015/10/how-securely-allow-users-upload-files
接受文件上传是一个冒险的主张,但只要采取一些基本的预防措施,就可以安全地执行此操作。也就是说,应当阻止意外地允许执行或解释上传文件的方式直接访问上传文件.
上传的文件应该是只读的或读写的,永远不能被执行.
如果你的站点根目录是 /var/www/example.com
, 你不应该存贮上传文件在 /var/www/example.com/uploaded_files
.
相反,你应该将上传文件存储在无法通过浏览器直接访问的单独目录中 (例如, /var/www/example.com-uploaded/
), 以免它们意外地作为服务器端脚本执行,打开远程代码执行的漏洞。
更简单的解决方案是将站点根目录向下移动一级 (例如,移动到:/var/www/example.com/public
)。
文件上传的另一个问题是安全地 下载文件 。
直接访问 SVG 图像将在用户的浏览器中执行 JavaScript 代码。 这是真的,尽管 SVG 图像的 MIME 类型是以image\ 为前缀.
MIME 类型嗅探可能导致类型混淆攻击。详情见前文安全 Headers 对 X-Content-Type-Options
响应头的介绍。
如果你不采纳前面关于如何安全存贮上传文件的建议,攻击者可能设法上传.php
或者.phtml
的文件, 通过直接在浏览器中访问上传文件执行任意代码,从而获得对服务器的完全控制。请安全地玩你的服务器。
跨站脚本 (XSS)
深入阅读: 关于防止 PHP 中的跨站点脚本漏洞,您需要了解的所有内容 https://paragonie.com/blog/2015/06/preventing-xss-vulnerabilities-in-php-everything-you-need-know
在理想的情况下, XSS 和 SQL 注入一样容易预防。我们有简单易用的 API ,用于将查询语句和数据进行分离。
不幸的是,大多数 web 开发人员的实际工作会涉及到将生成的一长串 HTML 文本并通过 HTTP 协议响应。这不是 PHP 独有的功能,它只是 web 开发的基本工作方式。
减少 XSS 漏洞并不是那么难做。但是,浏览器安全 的所有内容是至关重要。简而言之:
经常对输出转义,而不是对输入转义。 如果你在数据库中存储已过滤的数据,然后在一些地方发现 SQL 注入漏洞,那么攻击者可以通过恶意代码篡改可信的数据记录,从而完全绕过 XSS 保护。
如果你的框架中有一个提供自动上下文过滤的模板引擎,那可以使用它。它将会给你的框架的功能的安全性提供保障。
echo htmlentities($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
是一种安全有效的方式,它可以阻止对 UTF-8 编码的 web 页面的 XSS 攻击,但它不允许任何 HTML 标签内容输出。如果你的需求是允许你使用 Markdown 替代 HTML,不要使用 HTML 。
如果你需要允许一些 HTML 并且没有使用模板引擎(请参见 #1),那么就使用 HTML 净化器。 HTML 净化器不适用于将内容转义成 HTML 标签属性的场景。
对于用户提供的 URLs ,你应该只允许使用
http:
和https:
协议模式;不要用javascript:
。此外,使用 URL 编码方式编码用户的所有输入。
跨站请求伪造 (CSRF)
跨站请求伪造是一种混乱的代理攻击,通过这种攻击,你可以利用用户的权限,欺骗用户的浏览器执行恶意的 HTTP 请求来实现攻击者的目的。
这在一般情况下可通过两个简单步骤解决:
使用 HTTPS,这是前置条件。没有 HTTPS,任何防御都会变得脆弱,然而单纯的 HTTPS 并不能阻止 CSRF。
添加基本的请求 - 响应的身份验证。
在所有表单中添加一个隐藏的表单值。
用安全的随机加密字符串来填充这个值(称为令牌)。
验证表单中是否含有这个隐藏的值,并且校验是否与设置的一致。
我们编写了一个名为 Anti-CSRF 的库来更进一步了解如何防范 CSRF。
为了防止重复攻击,每个令牌只能使用一次。
多个令牌都存储在后端。
令牌轮换一旦达到上限,优先使用最老的。
每个令牌都可以绑定到指定的资源地址(URI)。
一旦令牌泄露,那么它也不能用于其他资源请求。
令牌可以选择绑定到指定的 IP 地址。
从 v2.1 版本开始,令牌可以重复使用(即 AJAX 调用)。
如果你没有使用一个能够帮你解决 CSRF 的框架,那么 Anti-CSRF 能够帮到你。
在将来,使用 cookie 的 SameSite 属性能够更加简单地防范 CSRF。
译者注:截至 2016 年 4 月,Chrome 51、Opera 39、火狐 60 已实现了 cookie 的 Same-Site 属性。
XML 攻击 (XML 外部实体注入,XPath 注入)
对于那些过多依赖 XML 数据结构来处理繁重的业务逻辑的程序而言,这里有两个比较容易被攻击的点。
XML 外部实体注入 (XXE)XPath 注入
XXE 攻击会利用服务器执行本地或远端的的恶意外部文件, 或者执行其他恶意的行为.
在早期版本的谷歌安全文档中就有显著的提及 XXE 攻击,但对于大多数的大量执行 XML 操作的非商业应用来说对 XXE 的防范意识是薄弱的。
其实最简单的减轻 XXE 攻击影响的方式就是:
libxml_disable_entity_loader(true);
XPath 注入 和 SQL 注入的原理一样,只不过是把攻击对象换成了 XML 文档。
比较幸运的是,PHP 对 XPath 操作方法的查询参数是很比较特殊和形式固定的。
另一方面呢,PHP 对于 XPath 注入没有提供简单有效的防御手段(参数过滤)。
对于 XPath 查询参数过滤,你的最佳方案就是使用白名单策略匹配过滤。
<?phpdeclare(strict_types=1);class SafeXPathEscaper{
/**
* @param string $input
* @return string
*/
public static function allowAlphaNumeric(string $input): string {
return \preg_replace('#[^A-Za-z0-9]#', '', $input);
}
/**
* @param string $input
* @return string
*/
public static function allowNumeric(string $input): string {
return \preg_replace('#[^0-9]#', '', $input);
}}// 用法示例:$selected = $xml->xpath(
"/user/username/" . SafeXPathEscaper::allowAlphaNumeric(
$_GET['username']
));
这里我们采用白名单的策略比黑名单策略安全。
反序列化和 PHP 对象注入
深度阅读: php 反序列化安全的实现 https://paragonie.com/blog/2016/04/securely-implementing-de-serialization-in-php
如果你将不可信的数据传递给 unserialize()
,你将不得不面对以下两个窘境:
PHP 对象注入风险,一种利用 POP 链(POP chain)来触发其他误用对象的漏洞。php 反序列化 POP 链的构造与理解异常语法
带来的 PHP 解析器本身的内存奔溃。
对于大多数开发者来讲他们更倾向于用 JSON 来替代对对象的序列化操作,这样的话确实规避了很多安全风险,但又引发了另外一个安全问题不得不提:DoS 攻击 - Hash 碰撞攻击( json_decode () 容易遭受 DoS 攻击 - Hash 碰撞攻击 (Hash-DoS) )。 遗憾的是, PHP 的 Hash-DOS 问题还没有得到彻底解决。
对于 Hash-DOS 利用的 php Hash 冲突的问题。从 djb33
迁移到 Siphash
,对于字符串输入,哈希输出的最高位设置为 1 ,对于整数输入设置为 0 ,使用 CSPRNG
提供的请求密钥,将完全解决这些攻击。
遗憾的是, PHP 团队还没有准备好放弃他们已经在 PHP 7 系列中取得的性能提升,所以很难说服他们放弃 djb33 (这是非常快但不安全的) 采用 SipHash (这也是快速的,但不像 djb33 那么快,但更安全)。 如果性能受到重大影响,可能会阻碍未来版本的采用,但也影响了安全性。
因此我们当前能做的是:
使用 JSON 的方式,因为它比
unserialize()
更安全。在任何可能的地方,在反序列化之前确保输入是经过安全过滤的。
对于提供给用户的数据,通过一个只有服务器知道的秘钥使用
sodium_crypto_auth()
和sodium_crypto_auth_verify()
验证。对于第三方提供的数据,让他们使用
sodium_crypto_sign()
签名他们的 JSON 消息,然后使用sodium_crypto_sign_open()
和第三方公钥验证消息。如果你需要对传输的签名进行十六进制或 Base64 位编码,也可以使用分离的签名 API 。
如果你无法确保 参数中 JSON 字符串的安全性,请严格限制单个 IP 访问频率以避免高频的攻击。(laravel 框架可以采用 Throttle 配置)
密码哈希
深入了解: 在 2016 年如何安全存储用户密码 https://paragonie.com/blog/2016/02/how-safely-store-password-in-2016
密码安全的存储曾经是一个备受争议的话题,但现在实现起来相当简单,尤其是在 PHP 中:
$hash = \password_hash($password, PASSWORD_DEFAULT);if (\password_verify($password, $hash)) {
// 已验证
if (\password_needs_rehash($hash, PASSWORD_DEFAULT)) {
// 刷新,更新数据库
}}
您甚至不需要知道后台使用的是什么算法,因为如果您使用的是最新版本的 PHP,那么您也将使用最新的最新版本,并且一旦有新的默认算法可用,用户的密码将自动升级。
不管你做什么, 在 WordPress 不要这样做.
如果你对此感到好奇:从 php 5.5 到 7.2,默认算法是 bcrypt。将来,它可能会切换到 Argon2,即 [密码散列类型]https://password-hashing.net/).
如果您以前没有在 API 中使用加密,同时需要迁移旧散列,请使用这种方法. 很多公司在这方面做了错误的操作,其中典型就的公司有,雅虎。, Yahoo. 最近,错误地实现旧哈希升级似乎已经导致了苹果最近的 iamroot
bug caused Apple's recent iamroot bug.
一般用途的加密
这是我们之前已经详细讨论过的话题:
正确的使用加密和身份验证 (2015)
推荐: 给你的 PHP 选择正确的加密库指南 (2015)
推荐: 你不能使用 Base64 加密一个密码 — 它可被解密 (2015)
可加密地安全的 PHP 开发 (2017)
推荐: Libsodium 快捷参考:名称相似的函数及其使用案例 (2017)
通常,你总希望使用 Sodium 加密库 (libsodium)对应用层加密。如果需要低于 7.2 版本(直到 5.2.4)你可以使用 sodium_compat 并假设你的用户也在使用 7.2 。
在特定的实例中,由于严格的算法选择和互操作性,你可能需要一个不同的库。如果有疑问,可以向加密专家咨询加密方式,并向密码工程师咨询这种实现是否安全。(这就是我们提供的服务之一 。)
如果你直接使用密码 / 模式, 请参考关于加密最佳实践的简要指南。
随机数
深入了解: 如何在 PHP 中安全地生成随机字符串和整数 https://paragonie.com/blog/2015/07/how-safely-generate-random-strings-and-integers-in-php
如果您需要随机数,请使用 random_int(). 如果需要随机字节字符串,请使用 random_bytes(). 所以不要使用 mt_rand()
, rand()
, 或 uniqid()
.
如果你需要从一个密钥生成伪随机数,不要用 srand()
或者 mt_srand()
, 请查看 SeedSpring .
<?php
use ParagonIE\SeedSpring\SeedSpring;
$seed = random_bytes(16);$rng = new SeedSpring($seed);
$data = $rng->getBytes(1024);
$int = $rng->getInt(1, 100);
服务器端 HTTPS 请求
简而言之:确保没有禁用 TLS 证书验证。
请随意使用你熟悉且兼容 PSR-7 规范的 HTTP 客户端,大多数人喜欢使用 Guzzle 库。也有一些人喜欢直接使用 cURL。
无论你用哪一种,你可以 使用 Certainty 库来确保你拥有最新的证书包 , 这进而允许你启用最严格的 TLS 证书验证设置,并保护服务器的出站 HTTPS 请求。
安装 Certainty 库非常简单:
composer require paragonie/certainty:^1
使用 Certainty 库也很容易:
<?php
use ParagonIE\Certainty\RemoteFetch;
$latestCACertBundle = (new RemoteFetch())->getLatestBundle();
# 使用 cURL:
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_CAINFO, $latestCACertBundle->getFilePath());
# 使用 Guzzle:
/** @var \GuzzleHttp\Client $http */
$repsonse = $http->get(
'https://example.com',
[
'verify' => $latestCACertBundle->getFilePath()
]
这将保护你免受 Web 服务器与你集成的任何第三方 API 之间的中间人攻击 (man-in-the-middle attacks)。
我们真的 需要 Certainty 库吗?
严格来说,为了保护您的系统,Certainty 库不是必要的。缺少它并不是弱点。
但是没有 Certainty 库。 开源软件必须推测操作系统的 CACert 包是否存在,并且如果它推测错误,它会经常失败得很惨并导致可用性问题。
纵观历史,这激励了许多开发人员禁用证书验证,这样他们的代码就可以 “正常工作” 了,却没有意识到他们的应用程序在受到主动攻击时是多么不堪一击。
Certainty 库通过使 CACert 捆绑包位于最新的和可预测的位置来消除这种激励。Certainty 库还为许多希望运行他们自己内部 CA 的企业提供了许多工具。
谁来禁用证书验证?
常见的内容管理系统(WordPress,Magento 等)的插件 / 扩展开发人员可以做到! 这是我们在生态系统层面试图解决的一个巨大问题。 它不是孤立于任何特定的 CMS,你会发现插件等。 因为所有这些都是不安全的。
如果您正在使用这样的 CMS,请在插件中搜索 CURLOPT_SSL_VERIFYPEER
和 CURLOPT_SSL_VERIFYHOST
,您可能会发现有几个将这些值设置为 FALSE
。
应该避免的事情
不要使用 mcrypt,一个已经十多年没有开发的密码学库。如果您正在关注 我们的 PHP 版本推荐,自从 PHP 7.2 及更新版本中没有提供 mcrypt,这应该是一个容易避开的陷阱。
配置驱动安全建议应该有大部分被忽视了。如果你正在阅读 PHP 安全指南并且他们告诉你修改 php.ini 配置,而不是编写更优秀的代码,那么你可能正在阅读非常过时的建议。 关掉当前窗口,并转移到那些不吹嘘 register_globals 的页面中。
不要使用 JOSE (JWT, JWS, JWE),尽管脚注表示已经写进了标准。但这是一套编纂成一系列容易出错的密码学设计,因为某些原因吸引了大量追崇者而形成的互联网标准 。
加密 URL 参数是一种很多公司用来混淆元数据采用的反面模式 (e.g. 我们有多少使用者呢?)。在产生安全感的错觉同时,它携带着实现错误的高风险。我们在相关文章中提出了一个更安全的替代方案。
除非你真的迫不得已,否则不要实现 "忘记密码" 功能。直言不讳地讲: 密码重置功能就是一个后门。 有一些方法可以实现它们,这些方法对合理的威胁模型是安全的,但是这应该也给了高风险用户一个让他们完全选择退出的机会。
如果可以,避免使用 RSA, 而使用 libsodium。如果你一定要 RSA,确保你指定了 OAEP 进行填补。
<?php
openssl_private_decrypt(
$ciphertext,
$decrypted, // 明文在成功时写入这个变量。
$privateKey,
OPENSSL_PKCS1_OAEP_PADDING // 重要:不要忽略这个!
);
如果你专注于使用 PKCS#1 v1.5 进行填补,无论你集成了什么,几乎都会很容易被 机器人攻击,所以将其作为一个允许明文公开和伪造签名的漏洞报告给合适的供应商 (或 US-CERT)。
专业使用案例
既然你已经掌握了在 2018 年及以后构建安全 PHP 应用程序的基础知识,现在让我们来看一些更专业的使用案例。
可搜索的加密
深入阅读:使用 PHP 和 SQL 建立可搜索的加密数据库 https://paragonie.com/blog/2017/05/building-searchable-encrypted-databases-with-php-and-sql
可搜索的加密数据库是可取的,但实现它被普遍认为不是非常重要。上面的博文试图让读者更深入地了解我们的解决方案,但实际上你只需:
设计您的架构,使数据库即使泄露也不会让攻击者获取到您的加密密钥。
使用密钥加密你的数据。
基于 HMAC 或带有静态盐的安全 KDF(例如:Argon2)创建多个索引(使用它们自己独特的密钥)。
可选:截断步骤 3 的输出,将其作为一个布隆过滤器(Bloom filter)。
在你的 SELECT 查询中使用第 3 或第 4 步的输出结果。
解密结果。
在这个过程中的任何步骤,你都可以根据你的实际情况来做调整。
无边信道的基于令牌的身份认证
深入阅读: 拆分令牌:无边信道的基于令牌的身份验证协议 https://paragonie.com/blog/2017/02/split-tokens-token-based-authentication-protocols-without-side-channels
说到数据库(上一章节),你知道 SELECT 查询理论上可以成为定时信息泄漏的来源吗?
简单的防范措施:
切分你的认证令牌。
一半在 SELECT 查询中使用。
在一定的时间内使用后半部分进行验。
您可以选择将后半部分的哈希存储在数据库中,而不是它本身。这对于只使用一次的令牌是有意义的,如用在 “密码重置” 或 “在这台计算机上记住我” 等地方的令牌。
这样即使你可能会因为定时泄漏被别人窃取到一半的令牌,剩下的一半也需要暴力破解才能成功。
开发安全的 API
深入阅读: 使用 Sapient 让 PHP 开发的 API 更为坚固 https://paragonie.com/blog/2017/06/hardening-your-php-powered-apis-with-sapient
我们编写了 SAPIENT(the Secure API ENgineering Toolkit)使服务器到服务器之间传递身份验证信息变得更为简单。
除了具有 HTTPS 提供的安全性之外,Sapient 还允许你使用共享密钥或公钥来加密和验证信息。
即使有一个用恶意 / 受损的证书颁发机构武装自己的中间人攻击(man-in-the-middle)攻击者,也能让你可以对 API 请求进行身份验证 ,并使用 ED25519 或只能由接收服务器的密钥解密的加密信息对目标服务器响应。
由于每个 HTTP 消息体都通过安全加密进行了身份验证,因此可以安全地使用它来代替有状态令牌可能会被篡改的协议(例如 OAuth)。但当涉及到密码学时,在做任何非标准的事情之前,都应该始终确保专家对它们的实现进行了研究。
Sapient 所使用的所有密码学算法都由 Sodium cryptography library 提供。
拓展阅读:
Sapient 文档
Sapient 教程
Sapient 规范
Paragon Initiative Enterprises 已经在其许多产品(包括许多开源软件项目)中使用了 Sapient,并将继续将软件项目添加到 Sapient 产品组合中。
使用 Chronicle 记录安全事件日志
深入阅读: Chronicle 会让你质疑区块链技术的需求 https://paragonie.com/blog/2017/07/chronicle-will-make-you-question-need-for-blockchain-technology
Chronicle 是一个基于哈希链(hash-chain)数据结构的仅追加的加密分类账(append-only cryptographic ledger),它具有许多吸引公司使用的 “区块链” 技术的属性,也不会有太多多余的功能。
除了仅追加加密分类账这样具有创造性的使用案例之外,当集成到 SIEM 中时,Chronicle 也是非常有亮点的,因为你可以将安全关键事件发送到私有的 Chronicle,使它们不可被改变。
如果你设置 Chronicle 将其摘要哈希交叉签名到其他 Chronicle 实例,或者配置了其他实例来复制 Chronicle 的内容,那么攻击者就很难篡改你的安全事件日志了。
使用 Chronicle,您可以获得区块链具有的所有弹性(resilience)特点,而不会出现任何隐私、性能和可扩展性问题。
要将数据发布到本地 Chronicle,您可以使用任何与 Sapient 兼容的 API,最简单的方式是 Quill。
作者的一些话
很多聪明的读者应该注意到了我们参考了很多我们自己的工作内容(包括了一些之前发的博文和我们的开源项目),但其实我们并不仅仅参考我们自己的内容。
这并不意外。
本公司自从 2015 成立以来一直致力于编写安全库并参与改善 PHP 生态系统安全性。
我们已经涉及了很多领域,但我们的安全工程师(就在最近发布的 PHP 7.2 中,他们推动了 PHP 核心更安全的加密技术的发展)自己并不太擅长炒作,也不会拘泥于过去所做的工作。很可能您甚至没有听说过我们多年来开发的一个半个工具或库。对此我们深感抱歉。
然而,我们也不可能成为所有方面的先驱,所以我们会尽可能地与一些符合公共利益而不是自私自利的专家们公事。这就是为什么在浏览器安全性的部分引用了 Scott Helme 和他的公司的工作内容,他们在使开发人员能够接触和理解这些新的安全特性方面做了大量的工作。
本篇指南也不是完全详尽的。写不安全的代码的方法几乎和写安全的代码的方法一样多。安全不仅仅是目的,更是一种心态。有了上面所写的内容,以及接下来的资源,我们希望这能帮助全世界的开发人员从今天起用 PHP 编写安全的软件。