介绍
以下文章是我电子书《Battle Ready Laravel》的节选,这是一本关于审计、测试、修复和提高您的Laravel应用程序的指南。
验证是任何Web应用程序的重要组成部分,可以帮助加强应用的安全性。但是,这也是经常被忽视或忘记的事情(至少在我审计的项目中,以及我加入的项目中)。
在这篇文章中,我们将简要探讨审计您应用程序的安全性或添加新验证时应注意的一些事情。我们还将探讨如何使用" Enlteh "检测潜在的批量赋值漏洞。
使用Enlightn进行批量赋值分析
什么是Enlightn?
我们可以用于深入了解我们项目的出色工具是Enlightn。
Enlightn是一个可以在命令行界面(CLI)中运行的应用程序,可以对您的Laravel项目提出改进性能和安全的建议。它不仅提供了一些静态分析工具,还执行了特定于Laravel项目的工具动态分析。因此,它生成的结果可以非常有用。
截至撰写本文时,Enlightn提供免费版和付费版。免费版提供64个检查,而付费版提供128个检查。
Enlightn 的一个有用之处在于,您不仅可以将其安装在您的生产服务器上(官方文档建议这样做),还可以安装到您的开发环境中。它不会给您的应用程序带来任何开销,因此它不应该影响您的项目性能,并且可以为您的服务器配置提供额外的分析。
使用批量赋值分析器
Enlightn 执行的有用分析之一是“批量赋值分析器”。它会扫描您的应用程序代码,以查找潜在的大规模赋值漏洞。
恶意用户可以利用大规模赋值漏洞来更改数据库中不应更改的数据的状态。为了了解这个问题,让我们看看我在过去的项目中遇到的一个潜在漏洞。
假设我们有一个 User
模型,它有几个字段:id
、name
、email
、password
、is_admin
、created_at
和 updated_at
。
想象一下,我们的项目用户界面有一个用户可以使用它来更新用户的表单。该表单只有两个字段:name
和 password
。处理此表单并更新用户的控制器方法可能如下所示
class UserController extends Controller
{
public function update(Request $request, User $user)
{
$user->update($request->all());
return redirect()->route('users.edit', $user);
}
}
上述代码会 正常工作,但是它容易被利用。例如,如果恶意用户尝试在请求体中传递一个 is_admin
字段,他们就可以改变一个不应被改变的字段。在这种情况下,这可能会导致权限提升漏洞,使非管理员可以把自己变成管理员。您可以想象,这可能导致数据保护问题和用户数据泄露。
Enlightn 文档提供了它能够检测到的多种大规模赋值用法的示例
$user->forceFill($request->all())->save();
User::update($request->all());
User::firstOrCreate($request->all());
User::upsert($request->all(), []);
User::where('user_id', 1)->update($request->all());
重要的是要记住,如果您在模型上定义了 $fillable
字段,这个问题就会小得多。例如,如果我们想声明在批量赋值值时,只有 name
和 email
字段可以更新,我们可以将用户模型更新如下
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
];
}
这意味着如果恶意用户尝试在请求中传递 is_admin
字段,当尝试将其分配给 User
模型时,该字段将被忽略。
但是,我建议您完全避免使用 all
方法来批量赋值,并且只在 Request
上使用 validated
、safe
或 only
方法。通过这样做,您始终可以确信您明确定义了您正在分配的字段。例如,如果我们想将我们的控制器更新为使用 only
,它可能如下所示
class UserController extends Controller
{
public function update(Request $request, User $user)
{
$user->update($request->only(['name', 'email']));
return redirect()->route('users.edit', $user);
}
}
如果我们想将我们的代码更新为使用 validated
或 safe
方法,我们首先需要创建一个表单请求类。在这个例子中,我们将创建一个 UpdateUserRequest
use Illuminate\Foundation\Http\FormRequest;
class UpdateUserRequest extends FormRequest
{
// ...
public function rules(): array
{
return [
'email' => 'required|email|max:254',
'name' => 'required|string|max:200',
];
}
}
我们可以通过将表单请求类型提示放在 update
方法的签名中来更改我们的控制器方法,并使用 validated
方法
use App\Http\Requests\UpdateUserRequest;
class UserController extends Controller
{
public function update(UpdateUserRequest $request, User $user)
{
$user->update($request->validated());
return redirect()->route('users.edit', $user);
}
}
或者,我们也可以这样使用 safe
方法
use App\Http\Requests\UpdateUserRequest;
class UserController extends Controller
{
public function update(UpdateUserRequest $request, User $user)
{
$user->update($request->safe());
return redirect()->route('users.edit', $user);
}
}
验证检查
当审计您的应用程序时,您需要检查每个控制器方法以确保请求数据被正确验证。正如我们在前面关于 Enlightenment 提供的批量赋值分析示例中已经讨论过的,我们知道在控制器中使用的所有数据都应经过验证,并且我们不应相信用户提供的数据。
每次检查控制器时,你必须问自己"我是否确定这个请求数据已经被验证并且可以安全存储?"。正如我们之前简要介绍的那样,记住客户端验证不能替代服务器端验证;两者应同时使用。例如,在我之前的项目中,我没有看到对“日期”字段的任何服务器端验证,因为表单提供了一个日期选择器,所以原始开发者认为这足以阻止用户发送除日期以外的任何数据。结果,这意味着可能向该字段传递不同类型的数据,这些数据可能会意外地或恶意地存储在数据库中。
应用基本规则
在验证字段时,我尽量应用以下四种类型的基本规则。
-
这道字段是必填的吗? - 我们是否期望这个字段在请求中始终存在?如果是这样,我们可以应用
required
规则。如果不是,我们可以使用nullable
或有时
规则。 -
这个字段的数据类型是什么? - 我们是否期望电子邮件、字符串、整数、布尔值或文件?如果是这样,我们可以应用
email
、string
、integer
、boolean
或files
规则。 - 这个字段有最小值(或长度)限制吗? - 例如,如果我们正在为产品目录页面添加价格过滤器,我们不希望用户将价格范围设置为-1,而希望它不低于0。
- 这个字段有最大长度(或长度)限制吗? - 例如,如果我们有一个产品页面“创建”表单,我们可能不希望标题超过100个字符。
因此,你可以把你的验证规则想象成使用以下格式编写的。
REQUIRED|DATATYPE|MIN|MAX|OTHERS
应用这些规则不仅可以为你的安全措施增加一些基本标准,还可以提高代码的可读性和提交数据的质量。
例如,假设我们在一个请求中具有以下字段
'name',
'publish_at',
'description',
'canonical_url',
虽然你可能能够猜测这些字段和它们的数据类型,但你可能无法回答上述每个字段的四个问题。然而,如果我们把这些四个问题应用到这些字段,并应用必要的规则,那么这些字段在我们的请求中可能看起来是这样的。
'name' => 'required|string|max:200',
'publish_at' => 'required|date|after:now',
'description' => 'required|string|min:50|max:250',
'canonical_url' => 'nullable|url',
现在我们添加了这些规则,我们对请求中的四个字段有了更多的信息,并且在控制器中处理它们时可以期待什么。这可以使得用户开发和后续工作的代码更容易,因为他们可以更清楚地了解请求包含的内容。
将这四个问题应用于所有请求字段可能非常有价值。如果您发现任何尚未进行此验证的请求,或者发现请求中的字段缺少上述任何规则,我建议您添加它们。但重要的是要注意,这些仅是基本要求,而且在大多数情况下,您可能还需要使用额外的规则以确保更加安全。
检查空验证
我在审计过的项目中发现,使用空规则进行字段验证是一个相当常见的问题。为了更好地理解这个问题,让我们看看一个简单的用于将用户存储在数据库中的控制器示例。
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email',
]);
$user = User::create($this->validated());
return redirect()->route('users.show', $user);
}
}
UserController
中的store
方法正在使用经过验证的数据(在这种情况下,是name
和email
),创建一个用户,然后返回一个重定向响应到用户的“展示”页面。到目前为止,还没有问题。
然而,假设这最初是在几个月前编写的,现在我们想要添加一个新的twitter_handle
字段。在我审计过的某些项目中,我发现添加了字段到验证,但没有应用任何规则,如下所示
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email',
'twitter_handle' => '',
]);
$user = User::create($this->validated());
return redirect()->route('users.show', $user);
}
}
这意味着现在可以在请求中输入并存储 twitter_handle
字段。这可能是一个危险的行为,因为它绕过了在存储之前验证数据的目的。
开发者可能这样做是为了快速构建功能,然后忘记在提交代码前添加规则。也可能是因为开发者认为这没有必要。然而,正如我们所说的,我们应该在服务器端对所有数据进行验证,以确保我们处理的数据始终是可接受的。
如果你遇到了这样的空验证规则集,你可能想添加规则以确保字段得到覆盖。你还可以检查数据库以确保没有存储无效数据。
结论
希望这篇帖子能让你对审计你的 Laravel 应用程序的验证时需要关注的一些事情有所了解。
如果你喜欢阅读这篇文章,我很乐意听到你的意见。同样,如果你有改进未来文章的反馈,我也很乐意听到。
你可能还对我们220多页的电子书《Battle Ready Laravel》感兴趣,该书中更深入地讨论了类似的主题。
如果你想每次我发布新文章时都收到更新,请随意 订阅我的通讯。
继续构建精彩的东西!🚀
driesvints, elkdev 喜欢这篇文章