跳到内容

Laravel 速查表

简介

速查表旨在为开发 Laravel 应用程序的开发者提供安全提示。它旨在涵盖所有常见的漏洞以及如何确保您的 Laravel 应用程序安全。

Laravel 框架提供了内置的安全功能,并且默认情况下是安全的。然而,它也为复杂的用例提供了额外的灵活性。这意味着不熟悉 Laravel 内部工作原理的开发者可能会陷入以不安全的方式使用复杂功能的陷阱。本指南旨在教育开发者避免常见陷阱,并以安全的方式开发 Laravel 应用程序。

您也可以参考Enlightn 安全文档,其中重点介绍了常见漏洞以及保护 Laravel 应用程序的良好实践。

基础

  • 确保您的应用程序在生产环境中未处于调试模式。要关闭调试模式,请将您的 APP_DEBUG 环境变量设置为 false
APP_DEBUG=false
  • 确保您的应用程序密钥已生成。Laravel 应用程序使用应用程序密钥进行对称加密和 SHA256 哈希,例如 Cookie 加密、签名 URL、密码重置令牌和会话数据加密。要生成应用程序密钥,您可以运行 key:generate Artisan 命令
php artisan key:generate
  • 确保您的 PHP 配置是安全的。您可以参考PHP 配置速查表了解更多关于安全 PHP 配置设置的信息。

  • 为您的 Laravel 应用程序设置安全的文件和目录权限。通常,所有 Laravel 目录的最大权限级别应设置为 775,非可执行文件的最大权限级别为 664。Artisan 或部署脚本等可执行文件应提供最大权限级别 775

  • 确保您的应用程序没有易受攻击的依赖项。您可以使用 Enlightn 安全检查器来检查。

默认情况下,Laravel 以安全的方式配置。但是,如果您更改了 Cookie 或会话配置,请确保以下几点

  • 如果您使用 cookie 会话存储或存储任何不应被客户端读取或篡改的数据,请启用 Cookie 加密中间件。通常,除非您的应用程序有非常特定的用例需要禁用此功能,否则应启用此功能。要启用此中间件,只需将 EncryptCookies 中间件添加到 App\Http\Kernel 类中的 web 中间件组即可
/**
 * The application's route middleware groups.
 *
 * @var array
 */
protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        ...
    ],
    ...
];
  • 通过 config/session.php 文件在会话 Cookie 上启用 HttpOnly 属性,以便您的会话 Cookie 无法通过 JavaScript 访问
'http_only' => true,
  • 除非您在 Laravel 应用程序中使用子域名路由注册,否则建议将 Cookie 的 domain 属性设置为 null,以便只有同源(不包括子域名)可以设置 Cookie。这可以在 config/session.php 文件中配置
'domain' => null,
  • 在您的 config/session.php 文件中将会话的 SameSite Cookie 属性设置为 laxstrict,以将会话 Cookie 限制为第一方或同站点上下文
'same_site' => 'lax',
  • 如果您的应用程序仅限 HTTPS,建议将 config/session.php 文件中的 secure 配置选项设置为 true,以防范中间人攻击。如果您的应用程序同时支持 HTTP 和 HTTPS,则建议将此值设置为 null,以便在处理 HTTPS 请求时自动设置 secure 属性
'secure' => null,
  • 确保您的会话空闲超时值较低。OWASP 建议高价值应用程序的空闲超时为 2-5 分钟,低风险应用程序为 15-30 分钟。这可以在 config/session.php 文件中配置
'lifetime' => 15,

您也可以参考Cookie 安全指南,了解更多关于 Cookie 安全和上述 Cookie 属性的信息。

认证

守卫和提供者

Laravel 的认证功能核心由“守卫(guards)”和“提供者(providers)”组成。守卫定义了如何对每个请求的用户进行认证。提供者定义了如何从持久化存储中检索用户。

Laravel 附带了一个使用会话存储和 Cookie 维护状态的 session 守卫,以及一个用于 API 令牌的 token 守卫。

对于提供者,Laravel 附带了一个用于使用 Eloquent ORM 检索用户的 eloquent 提供者,以及一个用于使用数据库查询构建器检索用户的 database 提供者。

守卫和提供者可以在 config/auth.php 文件中配置。Laravel 还提供了构建自定义守卫和提供者的能力。

入门套件

Laravel 提供了多种包含内置认证功能的第一方应用程序入门套件

  1. Laravel Breeze:一个简单、最小化的 Laravel 所有认证功能实现,包括登录、注册、密码重置、电子邮件验证和密码确认。
  2. Laravel Fortify:一个无头认证后端,包括上述认证功能以及两步认证。
  3. Laravel Jetstream:一个应用程序入门套件,在 Laravel Fortify 的认证功能之上提供了一个 UI。

建议使用这些入门套件之一,以确保 Laravel 应用程序的强大和安全认证。

API 认证包

Laravel 还提供以下 API 认证包

  1. Passport:一个 OAuth2 认证提供者。
  2. Sanctum:一个 API 令牌认证提供者。

Fortify 和 Jetstream 等入门套件内置了对 Sanctum 的支持。

批量赋值

批量赋值是现代 Web 应用程序中使用 ORM(如 Laravel 的 Eloquent ORM)时常见的漏洞。

批量赋值是一种漏洞,通过滥用 ORM 模式来修改用户通常不应被允许修改的数据项。

考虑以下代码

Route::any('/profile', function (Request $request) {
    $request->user()->forceFill($request->all())->save();

    $user = $request->user()->fresh();

    return response()->json(compact('user'));
})->middleware('auth');

上述资料路由允许已登录用户更改其资料信息。

然而,假设用户表中有个 is_admin 列。您可能不希望用户被允许更改此列的值。但是,上述代码允许用户更改用户表中其行中的任何列值。这是一个批量赋值漏洞。

Laravel 默认内置了防范此漏洞的功能。请确保以下几点以保持安全

  • 使用 $request->only$request->validated 来限定您希望更新的允许参数,而不是使用 $request->all
  • 不要解除模型的保护(unguard models)或将 $guarded 变量设置为空数组。这样做实际上是禁用了 Laravel 内置的批量赋值保护。
  • 避免使用 forceFillforceCreate 等绕过保护机制的方法。但是,如果您传入一个已验证的数组值,则可以使用这些方法。

SQL 注入

SQL 注入攻击在现代 Web 应用程序中不幸地相当常见,它们涉及攻击者提供恶意请求输入数据来干扰 SQL 查询。本指南涵盖了 SQL 注入以及如何专门针对 Laravel 应用程序进行预防。您还可以参考SQL 注入预防速查表了解更多不特定于 Laravel 的信息。

Eloquent ORM SQL 注入防护

默认情况下,Laravel 的 Eloquent ORM 通过参数化查询和使用 SQL 绑定来防范 SQL 注入。例如,考虑以下查询

use App\Models\User;

User::where('email', $email)->get();

上述代码会触发以下查询

select * from `users` where `email` = ?

因此,即使 $email 是不可信的用户输入数据,您也能受到 SQL 注入攻击的保护。

原始查询 SQL 注入

Laravel 还提供了原始查询表达式和原始查询来构建复杂的查询或开箱即用不支持的特定于数据库的查询。

虽然这对于灵活性很有帮助,但您必须小心,始终对此类查询使用 SQL 数据绑定。考虑以下查询

use Illuminate\Support\Facades\DB;
use App\Models\User;

User::whereRaw('email = "'.$request->input('email').'"')->get();
DB::table('users')->whereRaw('email = "'.$request->input('email').'"')->get();

这两行代码实际上执行相同的查询,由于查询没有对不可信的用户输入数据使用 SQL 绑定,因此容易受到 SQL 注入攻击。

上述代码会触发以下查询

select * from `users` where `email` = "value of email query parameter"

请务必记住对请求数据使用 SQL 绑定。我们可以通过进行以下修改来修复上述代码

use App\Models\User;

User::whereRaw('email = ?', [$request->input('email')])->get();

我们甚至可以像这样使用命名 SQL 绑定

use App\Models\User;

User::whereRaw('email = :email', ['email' => $request->input('email')])->get();

列名 SQL 注入

您绝不能允许用户输入数据来决定查询中引用的列名。

以下查询可能容易受到 SQL 注入

use App\Models\User;

User::where($request->input('colname'), 'somedata')->get();
User::query()->orderBy($request->input('sortBy'))->get();

需要注意的是,尽管 Laravel 具有一些内置功能,例如包装列名以防范上述 SQL 注入漏洞,但某些数据库引擎(取决于版本和配置)可能仍然容易受到攻击,因为数据库不支持绑定列名。

至少,这可能会导致批量赋值漏洞而不是 SQL 注入,因为您可能期望一组特定的列值,但由于此处未验证它们,用户可以自由使用其他列。

对于此类情况,请始终像这样验证用户输入

use App\Models\User;

$request->validate(['sortBy' => 'in:price,updated_at']);
User::query()->orderBy($request->validated()['sortBy'])->get();

验证规则 SQL 注入

某些验证规则可以选择提供数据库列名。此类规则与列名 SQL 注入以相同的方式容易受到 SQL 注入,因为它们以相似的方式构建查询。

例如,以下代码可能存在漏洞

use Illuminate\Validation\Rule;

$request->validate([
    'id' => Rule::unique('users')->ignore($id, $request->input('colname'))
]);

在幕后,上述代码会触发以下查询

use App\Models\User;

$colname = $request->input('colname');
User::where($colname, $request->input('id'))->where($colname, '<>', $id)->count();

由于列名是由用户输入决定的,它类似于列名 SQL 注入。

跨站脚本 (XSS)

XSS 攻击是注入攻击,恶意脚本(如 JavaScript 代码片段)被注入到可信网站中。

Laravel 的Blade 模板引擎具有 {{ }} 回显语句,会自动使用 htmlspecialchars PHP 函数转义变量,以防范 XSS 攻击。

Laravel 还提供了使用未转义语法 {!! !!} 来显示未转义数据的功能。这绝不能用于任何不可信的数据,否则您的应用程序将受到 XSS 攻击。

例如,如果您的任何 Blade 模板中有类似这样的内容,则会导致漏洞

{!! request()->input('somedata') !!}

然而,这样做是安全的

{{ request()->input('somedata') }}

有关不特定于 Laravel 的 XSS 防范的其他信息,您可以参考跨站脚本防范速查表

无限制文件上传

无限制文件上传攻击涉及攻击者上传恶意文件以破坏 Web 应用程序。本节描述了在构建 Laravel 应用程序时如何防范此类攻击。您还可以参考文件上传速查表了解更多信息。

始终验证文件类型和大小

始终验证文件类型(扩展名或 MIME 类型)和文件大小,以避免存储 DOS 攻击和远程代码执行

$request->validate([
    'photo' => 'file|size:100|mimes:jpg,bmp,png'
]);

存储 DOS 攻击利用缺失的文件大小验证,上传大量文件以耗尽磁盘空间,导致拒绝服务 (DOS)。

远程代码执行攻击首先上传恶意可执行文件(如 PHP 文件),然后通过访问文件 URL(如果公开)来触发其恶意代码。

通过上述简单的文件验证可以避免这两种攻击。

不要依赖用户输入来指定文件名或路径

如果您的应用程序允许用户控制数据来构建文件上传的路径,这可能会导致覆盖关键文件或将文件存储在错误的位置。

考虑以下代码

Route::post('/upload', function (Request $request) {
    $request->file('file')->storeAs(auth()->id(), $request->input('filename'));

    return back();
});

此路由将文件保存到特定于用户 ID 的目录中。在这里,我们依赖于 filename 用户输入数据,这可能会导致漏洞,因为文件名可能是 ../2/filename.pdf。这会将文件上传到用户 ID 2 的目录中,而不是当前登录用户所属的目录中。

为了解决这个问题,我们应该使用 basename PHP 函数从 filename 输入数据中剥离出任何目录信息

Route::post('/upload', function (Request $request) {
    $request->file('file')->storeAs(auth()->id(), basename($request->input('filename')));

    return back();
});

尽可能避免处理 ZIP 或 XML 文件

XML 文件可能使您的应用程序面临各种攻击,例如 XXE 攻击、十亿笑攻击等。如果您处理 ZIP 文件,则可能面临 zip bomb DOS 攻击。

请参阅XML 安全速查表文件上传速查表了解更多信息。

路径遍历

路径遍历攻击旨在通过操作带有 ../ 序列和变体或使用绝对文件路径的请求输入数据来访问文件。

如果您允许用户通过文件名下载文件,如果输入数据未剥离目录信息,则可能暴露于此漏洞。

考虑以下代码

Route::get('/download', function(Request $request) {
    return response()->download(storage_path('content/').$request->input('filename'));
});

在这里,文件名未剥离目录信息,因此像 ../../.env 这样的畸形文件名可能会将您的应用程序凭据暴露给潜在攻击者。

与无限制文件上传类似,您应该使用 basename PHP 函数来剥离目录信息,如下所示

Route::get('/download', function(Request $request) {
    return response()->download(storage_path('content/').basename($request->input('filename')));
});

开放重定向

开放重定向攻击本身并不危险,但它们会促成钓鱼攻击。

考虑以下代码

Route::get('/redirect', function (Request $request) {
   return redirect($request->input('url'));
});

此代码将用户重定向到用户输入提供的任何外部 URL。这可能使攻击者能够创建看似安全的 URL,例如 https://example.com/redirect?url=http://evil.com。例如,攻击者可能使用此类 URL 来伪造密码重置电子邮件,并引导受害者在攻击者的网站上暴露其凭据。

跨站请求伪造 (CSRF)

跨站请求伪造 (CSRF) 是一种攻击类型,当用户已通过认证时,恶意网站、电子邮件、博客、即时消息或程序导致用户的网络浏览器在受信任的网站上执行非预期操作。

Laravel 通过 VerifyCSRFToken 中间件提供开箱即用的 CSRF 保护。通常,如果您的 App\Http\Kernel 类中的 web 中间件组中有此中间件,您应该会受到良好的保护

/**
 * The application's route middleware groups.
 *
 * @var array
 */
protected $middlewareGroups = [
    'web' => [
        ...
         \App\Http\Middleware\VerifyCsrfToken::class,
         ...
    ],
];

接下来,对于所有 POST 请求表单,您可以使用 @csrf blade 指令生成隐藏的 CSRF 输入令牌字段

<form method="POST" action="/profile">
    @csrf

    <!-- Equivalent to... -->
    <input type="hidden" name="_token" value="{{ csrf_token() }}" />
</form>

对于 AJAX 请求,您可以设置 X-CSRF-Token 头部

Laravel 还提供了在 CSRF 中间件类中使用 $except 变量从 CSRF 保护中排除某些路由的能力。通常,您只希望将无状态路由(例如 API 或 Webhook)从 CSRF 保护中排除。如果排除任何其他路由,则可能导致 CSRF 漏洞。

命令注入

命令注入漏洞涉及执行由未转义的用户输入数据构建的 shell 命令。

例如,以下代码对用户提供的域名执行 whois 命令

public function verifyDomain(Request $request)
{
    exec('whois '.$request->input('domain'));
}

上述代码存在漏洞,因为用户数据未正确转义。为此,您可以使用 escapeshellcmd 和/或 escapeshellarg PHP 函数。

其他注入

对象注入、eval 代码注入和提取变量劫持攻击涉及对不可信的用户输入数据进行反序列化、评估或使用 extract 函数。

一些例子是

unserialize($request->input('data'));
eval($request->input('data'));
extract($request->all());

一般来说,避免将任何不可信的输入数据传递给这些危险函数。

安全头部

您应该考虑将以下安全头部添加到您的 Web 服务器或 Laravel 应用程序中间件中

  • X-Frame-Options
  • X-Content-Type-Options
  • Strict-Transport-Security (仅适用于 HTTPS 应用程序)
  • 内容安全策略

更多信息,请参考OWASP 安全头部项目

工具

您应该考虑使用 Enlightn,这是一个针对 Laravel 应用程序的静态和动态分析工具,拥有超过 45 项自动化安全检查,用于识别潜在的安全问题。Enlightn 提供了开源版本和商业版本。Enlightn 包含一份详尽的 45 页安全漏洞文档,了解 Laravel 安全的绝佳方式就是查看其文档

您还应该使用Enlightn 安全检查器本地 PHP 安全检查器。它们都是开源软件包,分别在 MIT 和 AGPL 许可下授权,使用安全咨询数据库扫描您的 PHP 依赖项是否存在已知漏洞。

参考资料