问题简而言之
我正在寻找一种方法来删除VerifyCsrfToken
来自包内的全局中间件管道without用户必须修改App\Http\Middleware\VerifyCsrfToken
。这可能吗?
用例
我正在开发一个包,可以轻松地将推送部署功能安全地添加到任何 Laravel 项目中。我从 Github 开始。Github 使用 webhook https://developer.github.com/webhooks/通知第三方应用程序有关事件的信息,例如推送或发布。换句话说,我会注册一个类似的 URLhttp://myapp.com/deploy http://myapp.com/deploy在 Github 上,Github 会发送一个POST
向该 URL 发出请求,其中包含事件发生时的详细信息,我可以使用该事件来触发新的部署。显然,我不想在 Github 服务之外的某些随机(或可能是恶意)代理访问该 URL 的情况下触发部署。因此,Github 有保护 Webhooks 的流程 https://developer.github.com/webhooks/securing/。这涉及到在 Github 上注册一个密钥,他们将使用该密钥发送一个特殊的、安全散列的标头以及您可以用来验证它的请求。
我确保安全的方法包括:
随机唯一 URL/路由和密钥
首先,我自动生成两个随机的、唯一的字符串,它们存储在.env
文件并用于在我的应用程序中创建密钥路由。在里面.env
文件看起来像这样:
AUTODEPLOY_SECRET=BHBfCiC0bjIDCAGH2I54JACwKNrC2dqn
AUTODEPLOY_ROUTE=UG2Yu8QzHY6KbxvLNxcRs0HVy9lQnKsx
The config
为此包创建两个密钥,auto-deploy.secret
and auto-deploy.route
我可以在注册路线时访问它,这样它就不会在任何存储库中发布:
Route::post(config('auto-deploy.route'),'MyController@index');
然后我可以访问 Github 并注册我的网络书,如下所示:
这样,部署 URL 和用于验证请求的密钥都将保持机密,并防止恶意代理在站点上触发随机部署。
用于验证 Webhook 请求的全局中间件
该方法的下一部分涉及为 Laravel 应用程序创建一个全局中间件,用于捕获和验证 Webhook 请求。我能够通过使用确保我的中间件在队列开头附近执行Laracasts 讨论线程中演示的方法 https://laracasts.com/discuss/channels/general-discussion/register-middleware-via-service-provider/?page=3。在里面ServiceProvider
对于我的包,我可以在前面添加一个新的全局中间件类,如下所示:
public function boot(Illuminate\Contracts\Http\Kernel $kernel)
{
// register the middleware
$kernel->prependMiddleware(Middleware\VerifyWebhookRequest::class);
// load my route
include __DIR__.'/routes.php';
}
My Route
好像:
Route::post(
config('auto-deploy.route'), [
'as' => 'autodeployroute',
'uses' => 'MyPackage\AutoDeploy\Controllers\DeployController@index',
]
);
然后我的中间件会实现一个handle()
方法看起来像这样:
public function handle($request, Closure $next)
{
if ($request->path() === config('auto-deploy.route')) {
if ($request->secure()) {
// handle authenticating webhook request
if (/* webhook request is authentic */) {
// continue on to controller
return $next($request);
} else {
// abort if not authenticated
abort(403);
}
} else {
// request NOT submitted via HTTPS
abort(403);
}
}
// Passthrough if it's not our secret route
return $next($request);
}
此功能一直有效,直到continue on to controller
bit.
问题的细节
当然,这里的问题是,由于这是一个POST
请求,并且没有session()
并且没有办法得到CSRF
提前代币,全球VerifyCsrfToken
中间件生成一个TokenMismatchException
并中止。我已经阅读了大量的论坛帖子,并深入研究了源代码,但我找不到任何干净、简单的方法来禁用VerifyCsrfToken
用于这一请求的中间件。我尝试了几种解决方法,但由于各种原因我不喜欢它们。
解决方法尝试#1:让用户修改VerifyCsrfToken
中间件
解决此问题的记录和支持的方法是将 URL 添加到$except
数组中的App\Http\Middleware\VerifyCsrfToken
类,例如
// The URIs that should be excluded from CSRF verification
protected $except = [
'UG2Yu8QzHY6KbxvLNxcRs0HVy9lQnKsx',
];
显然,这样做的问题是,当这段代码被签入存储库时,任何碰巧查看的人都会看到它。为了解决这个问题,我尝试过:
protected $except = [
config('auto-deploy.route'),
];
但 PHP 不喜欢它。我还尝试在这里使用路线名称:
protected $except = [
'autodeployroute',
];
但这也行不通。它必须是实际的 URL。其实那件事确实有效是重写构造函数:
protected $except = [];
public function __construct(\Illuminate\Contracts\Encryption\Encrypter $encrypter)
{
parent::__construct($encrypter);
$this->except[] = config('auto-deploy.route');
}
但这必须是安装说明的一部分,并且对于 Laravel 包来说是一个不寻常的安装步骤。我有一种感觉,这就是我最终会采用的解决方案,因为我想要求用户这样做并不是那么困难。它的好处是至少可能让他们意识到他们将要安装的软件包会规避 Laravel 的一些内置安全性。
解决方法尝试#2:catch
the TokenMismatchException
我尝试的下一件事是看看我是否可以捕获异常,然后忽略它并继续,即:
public function handle($request, Closure $next)
{
if ($request->secure() && $request->path() === config('auto-deploy.route')) {
if ($request->secure()) {
// handle authenticating webhook request
if (/* webhook request is authentic */) {
// try to continue on to controller
try {
// this will eventually trigger the CSRF verification
$response = $next($request);
} catch (TokenMismatchException $e) {
// but, maybe we can just ignore it and move on...
return $response;
}
} else {
// abort if not authenticated
abort(403);
}
} else {
// request NOT submitted via HTTPS
abort(403);
}
}
// Passthrough if it's not our secret route
return $next($request);
}
是的,现在就来嘲笑我吧。傻兔,不是这样的try/catch
作品!当然$response
内未定义catch
堵塞。如果我尝试做$next($request)
in the catch
块,它只是撞击TokenMismatchException
again.
解决方法尝试#3:在中间件中运行我的所有代码
当然,我可以忘记使用Controller
用于部署逻辑并触发中间件的所有内容handle()
方法。请求生命周期将在那里结束,我永远不会让中间件的其余部分传播。我不禁觉得这有些不优雅,而且它偏离了 Laravel 构建的整体设计模式,以至于最终导致维护和协作变得困难。至少我知道它会起作用。
解决方法尝试#4:修改Pipeline
Philip Brown 有一篇很棒的教程,描述了管道模式 http://culttt.com/2015/09/28/how-to-use-the-pipeline-design-pattern-in-laravel/以及它是如何在 Laravel 中实现的。 Laravel 的中间件使用了这种模式。我想也许,只是也许,有一种方法可以访问Pipeline
对象对中间件包进行排队,循环遍历它们,并删除我的路由的 CSRF 包。据我所知,有办法add管道中添加了新元素,但无法找出其中的内容或以任何方式对其进行修改。如果您知道方法,请告诉我!
解决方法尝试#5:使用WithoutMiddleware
trait
我还没有彻底研究过这个问题,但似乎最近添加了这一特性,以便允许测试路由而不必担心中间件。它显然不适合生产,禁用中间件意味着我必须想出一个全新的解决方案来弄清楚如何让我的包完成它的任务。我决定这不是我该走的路。
解决方法尝试#6:放弃。只需使用Forge https://forge.laravel.com/ or Envoyer https://envoyer.io/
为什么要重新发明轮子?为什么不直接为这些已经支持推送部署的服务中的一项或两项付费,而不是麻烦地滚动我自己的软件包呢?嗯,首先,我每月只为我的服务器支付 5 美元,因此每月为其中一项服务另外支付 5 美元或 10 美元在经济上感觉不太合适。我是一名老师,他开发应用程序来支持我的教学。它们都不产生收入,虽然我可能负担得起,但随着时间的推移,这种事情会增加。
讨论
好吧,我花了两天的大部分时间来解决这个问题,这就是我来这里寻求帮助的原因。你有解决方案吗?如果您已经读到这里,也许您会沉迷于一些结束语。
想法#1:为 Laravel 人员认真对待安全性而喝彩!
编写一个绕过内置安全机制的包是多么困难,这给我留下了深刻的印象。我并不是以“我试图做坏事”的方式谈论“规避”,而是从某种意义上说,我正在尝试编写一个合法的软件包,以节省我和许多其他人的时间,但实际上会要求他们“相信我”其应用程序的安全性,因为这可能会导致恶意部署触发器。这应该很难做到正确,而且确实如此。
想法#2:也许我不应该正在做这个
通常,如果某些事情很难或不可能在代码中实现,那就是设计使然。也许我想自动化这个包的整个安装过程是因为 Bad Design™。也许这就是代码告诉我,“不要这样做!”你怎么认为?
总结来说,这里有两个问题:
- 你知道一种我没有想到的方法吗?
- 这是糟糕的设计吗?我不应该这样做吗?
感谢您的阅读,也感谢您的深思熟虑的回答。
附:在有人说之前我就知道这可能是重复的 https://stackoverflow.com/questions/33177674/is-there-a-way-to-exclude-a-route-from-csrf-protection-from-within-a-package-in,但我提供了比另一位发帖者更多的细节,他也从未找到解决方案。