以编程方式从 Laravel 包中添加 CSRF 检查异常

Programmatically add exception from CSRF check from Laravel package

问题简述

我正在寻找一种方法来从包 中的全局中间件管道中删除 VerifyCsrfToken 而无需 用户修改 App\Http\Middleware\VerifyCsrfToken。这可能吗?

用例

我正在开发一个包,可以轻松安全地将 push-to-deploy 功能添加到任何 Laravel 项目。我从 Github 开始。 Github uses webhooks to notify 3rd party apps about events, such as pushes or releases. In other words, I would register a URL like http://myapp.com/deploy at Github, and Github will send a POST request to that URL with a payload containing details about the event whenever it happens, and I could use that event to trigger a new deployment. Obviously, I don't want to trigger a deployment on the off chance that some random (or perhaps malicious) agent other than the Github service hits that URL. As such, Github has a process for securing your webhooks。这涉及使用 Github 注册一个密钥,他们将使用该密钥发送一个特殊的、安全散列的 header 以及您可以用来验证它的请求。

我确保此安全的方法包括:

随机唯一URL/Route和密钥

首先,我自动生成两个随机的、唯一的字符串,它们存储在 .env 文件中,用于在我的应用程序中创建密钥路由。在 .env 文件中,它看起来像:

AUTODEPLOY_SECRET=BHBfCiC0bjIDCAGH2I54JACwKNrC2dqn
AUTODEPLOY_ROUTE=UG2Yu8QzHY6KbxvLNxcRs0HVy9lQnKsx

这个包的 config 创建了两个键, auto-deploy.secretauto-deploy.route 我可以在注册路由时访问它们,这样它就不会在任何 repo 中发布:

Route::post(config('auto-deploy.route'),'MyController@index');

然后我可以去 Github 并像这样注册我的网络书:

这样,部署 URL 和用于验证请求的密钥都将保密,并防止恶意代理在站点上触发随机部署。

用于验证 Webhook 请求的全局中间件

该方法的下一部分涉及为 Laravel 应用程序创建一个全局中间件,用于捕获和验证 webhook 请求。通过使用 an approach demonstrated in this Laracasts discussion thread,我能够确保我的中间件在 queue 开始附近执行。在我的包的 ServiceProvider 中,我可以预先添加一个新的全局中间件 class,如下所示:

public function boot(Illuminate\Contracts\Http\Kernel $kernel)
{
    // register the middleware
    $kernel->prependMiddleware(Middleware\VerifyWebhookRequest::class);
    // load my route
    include __DIR__.'/routes.php';
}

我的 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 位。

详细问题

当然这里的问题是因为这是一个POST请求,没有session()也没有办法提前得到一个CSRFtoken,全局VerifyCsrfToken 中间件生成 TokenMismatchException 并中止。我已经阅读了许多论坛帖子,并深入研究了源代码,但我找不到任何干净简单的方法来为这个请求禁用 VerifyCsrfToken 中间件。我尝试了几种变通方法,但出于各种原因我不喜欢它们。

解决方法尝试 #1:让用户修改 VerifyCsrfToken 中间件

解决此问题的记录和支持方法是将 URL 添加到 App\Http\Middleware\VerifyCsrfToken class 中的 $except 数组,例如

// 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 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 的工作方式!当然 $responsecatch 块中是未定义的。如果我尝试在 catch 块中执行 $next($request),它只会再次与 TokenMismatchException 碰撞。

解决方法尝试 #3:运行 我在中间件中的所有代码

当然,我可以忘记为部署逻辑使用 Controller 并从中间件的 handle() 方法触发所有内容。请求生命周期将在那里结束,我永远不会让其余的中间件传播。我不禁觉得这有些不雅,而且它与构建 Laravel 的整体设计模式背道而驰,最终会使维护和协作变得困难。至少我知道它会起作用。

解决方法尝试 #4:修改 Pipeline

Philip Brown has an excellent tutorial describing the Pipeline pattern 及其在 Laravel 中的实现方式。 Laravel的中间件就使用了这个模式。我想也许,只是也许,有一种方法可以访问 Pipeline object queue 中间件包,循环遍历它们,并为我的路由删除 CSRF .据我所知,有一些方法可以 添加 新元素到 ppeline,但无法找出其中的内容或以任何方式修改它。如果你知道方法,请告诉我!!!

解决方法尝试 #5:使用 WithoutMiddleware 特征

我还没有对这个进行彻底的调查,但似乎最近添加了这个特征以允许测试路由而不必担心中间件。它显然不适合生产,禁用中间件意味着我必须想出一个全新的解决方案来弄清楚如何让我的包做它的事情。我决定这不是要走的路。

解决方法尝试 #6:放弃。只需使用 Forge or Envoyer

为什么要重新发明轮子?为什么不只为已经支持 push-to-deploy 的一项或两项服务付费,而不是麻烦自己滚动包裹呢?好吧,首先,我只为我的服务器支付 5 美元/月,所以不知何故每月为其中一项服务支付 5 美元或 10 美元的经济性感觉不对。我是一名教师,开发应用程序来支持我的教学。 None 他们产生了收入,虽然我可能负担得起,但随着时间的推移,这种事情会增加。

讨论

好的,所以我花了整整两天的大部分时间来解决这个问题,这就是我来这里寻求帮助的原因。你有解决方案吗?如果您已经读到这里,也许您会放纵一些结束语。

想法 #1:感谢 Laravel 认真对待安全的家伙!

我对编写绕过 built-in 安全机制的程序包的困难程度印象深刻。我不是在用 I'm-trying-to-do-something-bad 的方式谈论 "circumvention",而是在某种意义上,我正在尝试编写一个合法的包,它可以节省我和其他很多人的时间,但是,实际上,通过潜在地向恶意部署触发器开放它们,要求他们 "trust me" 确保其应用程序的安全性。这应该很难做到正确,但确实如此。

想法 #2:也许我不应该这样做

通常情况下,如果某些东西很难或不可能在代码中实现,那是设计使然。对我而言,可能是 Bad Design™ 想要自动化此软件包的整个安装过程。也许这是代码告诉我,"Don't do that!"你觉得怎么样?

综上所述,有两个问题:

  1. 你知道我没有想到的方法吗?
  2. 这是糟糕的设计吗?我不应该这样做吗?

感谢阅读,感谢您的周到解答。

P.S。在有人说之前,我知道 ,但我提供的信息比其他发帖者详细得多,他也从未找到解决方案。

我知道在生产代码中使用反射 API 不是好的做法,但这是我能想到的唯一不需要额外配置的解决方案。这更像是概念验证,我不会在生产代码中使用它。

我认为更好更稳定的解决方案是让用户更新他的中间件以使用您的包。

tl;dr - 你可以把它放在你的包引导代码中:

// Just remove CSRF middleware when we hit the deploy route
if(request()->is(config('auto-deploy.route')))
{
    // Create a reflection object of the app instance
    $appReflector = new ReflectionObject(app());

    // When dumping the App instance, it turns out that the
    // global middleware is registered at:
    // Application
    //  -> instances
    //   -> Illuminate\Contracts\Http\Kernel
    //    -> ... Somewhere in the 'middleware' array
    //
    // The 'instance' property of the App object is not accessible
    // by default, so we have to make it accessible in order to
    // get and set its value.
    $instancesProperty = $appReflector->getProperty('instances');
    $instancesProperty->setAccessible(true);
    $instances = $instancesProperty->getValue(app());
    $kernel = $instances['Illuminate\Contracts\Http\Kernel'];

    // Now we got the Kernel instance.
    // Again, we have to set the accessibility of the instance.
    $kernelReflector = new ReflectionObject($kernel);
    $middlewareProperty = $kernelReflector->getProperty('middleware');
    $middlewareProperty->setAccessible(true);
    $middlewareArray = $middlewareProperty->getValue($kernel);

    // The $middlewareArray contains all global middleware.
    // We search for the CSRF entry and remove it if it exists.
    foreach ($middlewareArray as $i => $middleware)
    {
        if ($middleware == 'App\Http\Middleware\VerifyCsrfToken')
        {
            unset($middlewareArray[ $i ]);
            break;
        }
    }

    // The last thing we have to do is to update the altered
    // middleware array on the Kernel instance.
    $middlewareProperty->setValue($kernel, $middlewareArray);
}

我还没有用 Laravel 5.1 测试过这个 - 对于 5.2 它有效。

因此您可以创建一个 Route::group,您可以在其中明确说明要使用哪个中间件。

例如,在您的 ServiceProvider 中,您可以这样做:

    \Route::group([
        'middleware' => ['only-middleware-you-need']
    ], function () {
        require __DIR__ . '/routes.php';
    });

所以只需要排除VerifyCsrfToken中间件,放上你需要的。