以编程方式从Laravel软件包中添加来自CSRF检查的异常 [英] Programmatically add exception from CSRF check from Laravel package

查看:166
本文介绍了以编程方式从Laravel软件包中添加来自CSRF检查的异常的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

简单问题



我正在寻找一种方法从全局中间件中移除 VerifyCsrfToken 如果没有用户必须修改 App \Http\Middleware\VerifyCsrfToken ,那么这个管道就是来自包内的管道。这是可能的吗?



用例



我正在开发一个可以使安全为任何Laravel项目添加推送部署功能。我从Github开始。

通过这种方式,部署URL和用于验证请求的密钥将保持秘密,并防止恶意代理在网站上触发随机部署。



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



该方法的下一部分涉及为Laravel应用程序创建一个全局中间件,以捕获并验证webhook请求。我可以通过使用在此Laracasts讨论主题中演示的方法。在我的软件包的 ServiceProvider 中,我可以添加一个新的全局中间件类,如下所示:

  public function boot(Illuminate \Contracts\Http\Kernel $ kernel)
{
//注册中间件
$ kernel-> prependMiddleware(Middleware \ VerifyWebhookRequest ::类);
//载入我的路线
include __DIR __。'/ routes.php';

我的路线长相例如:

  Route :: post(
config('auto-deploy.route'),[
'as'=>'autodeployroute',
'uses'=>'MyPackage\AutoDeploy\Controllers\DeployController @index',
]
);

然后我的中间件会执行 handle()
$ b

  public function handle($ request,Closure $ next)
{
if($ request-> path()=== config('auto-deploy.route')){
if($ request-> secure()){
// handle如果(/ * webhook请求是真实的* /){
//继续到控制器
返回$ next($ request),则认证webhook请求
;
} else {
//如果未经过认证,则中止
中止(403);
}
} else {
//请求不通过HTTPS提交
abort(403);
}
}
//直通,如果它不是我们的秘密路线
return $ next($ request);

$ / code>

直到继续控制器



问题详解



当然这里的问题是因为这是 POST 请求,并且没有 session()并且没有办法获得 CSRF 标记,全局 VerifyCsrfToken 中间件会生成一个 TokenMismatchException 并异常终止。我已经阅读了大量的论坛主题,并通过源代码挖掘出来的,但是我找不到任何干净而简单的方法来为这个请求禁用 VerifyCsrfToken 中间件。我尝试了几种解决方法,但由于各种原因我不喜欢它们。



解决方法尝试#1:让用户修改 VerifyCsrfToken middleware



解决此问题的文档和支持的方法是将URL添加到 $,但数组在 App \Http\Middleware\VerifyCsrfToken 类中,例如

  //应该从CSRF验证中排除的URI 
保护$除了= [
'UG2Yu8QzHY6KbxvLNxcRs0HVy9lQnKsx',
];

显然,这个问题的原因是当这段代码被检入回购库时,任何碰巧看到的人都可以看到它。为了解决这个问题,我尝试过:

  protected $ except = [
config('auto-deploy.route')) ,
];

但是PHP不喜欢它。

 保护$除了= [
'autodeployroute',
]。

但这也行不通。它必须是实际的网址。实际上不起作用的是重写构造函数:

  protected $ except = []; 

public function __construct(\Illuminate\Contracts\Encryption\Encrypter $ encrypter)
{
parent :: __ construct($ encrypter);
$ this->除了[] = config('auto-deploy.route');
}

但这必须是安装说明的一部分,不寻常的Laravel软件包安装步骤。我有一种感觉,这是我最终会采用的解决方案,因为我猜想要求用户做到这一点并不难。至少可能让他们意识到他们即将安装的软件包会绕过Laravel的一些内置安全功能。

解决方法尝试#2: catch TokenMismatchException



接下来我尝试的是看看我是否可以捕获异常,然后忽略它并继续前进,即:
$ b $ pre $ public function handle($ request,Closure $()
{
if($ request-> secure()& $ amp; $ request-> path()=== config('auto-deploy.route')){
if($ request-> secure()){
//处理认证webhook请求
if(/ * webhook request is authentic * /){
//尝试继续到控制器
试试{
//这将最终触发CSRF验证
$ response = $ next($ request);
} catch(TokenMismatchException $ e){
//但是,也许我们可以忽略它并继续...
return $ response;
}
} else {
//如果未经过认证,则中止
abort(403);
}
} else {
//请求不通过HTTPS提交
abort(403);
}
}
//直通,如果它不是我们的秘密路线
return $ next($ request);
}

是的,现在就继续嘲笑我。愚蠢的wabbit,这不是如何 try / catch 的作品!当然 $ response catch 块中是未定义的。如果我在 catch 块中尝试执行 $ next($ request),它只会对<$ code> TokenMismatchException 再次。

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



当然,我可能忘记了使用 Controller 作为部署逻辑,并触发中间件的 handle() 方法。请求生命周期将在那里结束,而我绝不会让其他中间件传播。我不禁感到这件事有些不合理,并且它背离了Laravel的整体设计模式,以至于最终导致维护和协作难以推进。至少我知道它会工作。



解决方法尝试#4:修改管道



Philip Brown有一本很好的教程来描述Pipeline模式,以及它如何在Laravel中实现。 Laravel的中间件使用这种模式。我想也许,也许,有一种方法可以访问 Pipeline 对象,该对象将中间件包排队,循环遍历它们,并为我的路由删除CSRF。 。最好我可以告诉,有一些方法可以将新元素添加到管道中,但无法找到其中的内容或以任何方式对其进行修改。如果您知道某种方式,请让我知道!!!



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



我还没有仔细研究过这个,但是看起来这个特性最近被添加来允许测试路由而不必担心中间件。这显然不是为了生产,禁用中间件意味着我不得不想出一个全新的解决方案来弄清楚如何让我的软件包做到这一点。我认为这不是要走的路。



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



为什么要重新发明轮子?为什么不只为这些已经支持推送部署的服务支付费用,而不愿意为自己的软件包而烦恼呢?呃,其中之一,我只为我的服务器支付5美元/月的费用,所以不管怎么说,为这些服务中的一个支付每月5美元或10美元的经济效益并不合适。我是一名建立应用程序以支持我的教学的老师。讨论讨论

没有一个产生收入,尽管我可能负担得起,但这种情况会随着时间的推移而加起来。 b

好吧,所以我已经花了两天的好时光来敲我的头,以解决这个问题,这是我在这里寻找帮助的原因。你有解决方案吗?如果你已经读了这么多,也许你会沉迷于一些结束的想法。



想法#1:布拉沃给拉拉维尔家伙认真考虑安全问题! h3>

编写一个包含内置安全机制的软件包是多么困难,我对此印象深刻。我不是在谈论我在尝试做某事的坏方法中的规避,但从某种意义上说,我试图写一个合法的方案,这会让我和很多其他人时间,但实际上会让他们信任我,因为他们的应用程序的安全性可能会让他们打开恶意的部署触发器。这应该很难得到正确的,它是。



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



通常情况下,如果某些事情难以或不可能在代码中实现,那就是设计。也许这是糟糕的设计和交易;我想要自动完成这个包的整个安装过程。也许这是代码告诉我,不要这样做!你认为什么?

总之,这里有两个问题:


  1. 你知道一种方法来做到这一点,我没有想到吗?

  2. 这是不好的设计吗?我不应该这样做吗?

感谢您的阅读,并感谢您深思熟虑的答复。

PS在别人说出来之前,我知道这可能是重复的,但我提供了比其他海报更多的细节,他也从来没有找到解决方案。

我知道在生产代码中使用Reflection API并不是一个好习惯,但这是我能想到的唯一不需要额外配置的解决方案。这更像是一个概念证明,我不会在生产代码中使用它。



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



tl; dr - 你可以把它放在你的软件包中启动代码:

<$ p $如果(request() - >是(config('auto-deploy.route')))
,那么当我们点击部署路由时,只需移除CSRF中间件
{
//创建应用程序实例的反射对象
$ appReflector = new ReflectionObject(app());

//当转储App实例时,事实证明
//全局中间件注册在:
// Application
// - >实例
// - > Illuminate \Contracts\Http\Kernel
// - > ...'中间件'数组中的某处
//
// App对象的'instance'属性默认情况下不可访问
//,因此我们必须使其可访问为了
//获取并设置其值。
$ instancesProperty = $ appReflector-> getProperty('instances');
$ instancesProperty-> setAccessible(true);
$ instances = $ instancesProperty-> getValue(app());
$ kernel = $ instances ['Illuminate\Contracts\Http\Kernel'];

//现在我们得到了Kernel实例。
//同样,我们必须设置实例的可访问性。
$ kernelReflector = new ReflectionObject($ kernel);
$ middlewareProperty = $ kernelReflector-> getProperty('middleware');
$ middlewareProperty-> setAccessible(true);
$ middlewareArray = $ middlewareProperty-> getValue($ kernel);

// $ middlewareArray包含所有全局中间件。
//我们搜索CSRF条目并将其删除(如果存在)。
foreach($ middlewareArray as $ i => $ middleware)
{
if($ middleware =='App\Http\Middleware\VerifyCsrfToken')
{
unset($ middlewareArray [$ i]);
休息;
}
}

//我们要做的最后一件事是更新内核实例上已更改的
//中间件数组。
$ middlewareProperty-> setValue($ kernel,$ middlewareArray);
}


The Problem in a Nutshell

I'm looking for a way to remove VerifyCsrfToken from the global middleware pipeline from within a package without the user having to modify App\Http\Middleware\VerifyCsrfToken. Is this possible?

The Use Case

I'm developing a package that would make it easy to securely add push-to-deploy functionality to any Laravel project. I'm starting with 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. This involves registering a secret key with Github that they will use to send a special, securely hashed header along with the request that you can use to verify it.

My approach to making this secure involves:

Random Unique URL/Route and Secret Key

First, I automatically generate two random, unique strings, that are stored in the .env file and used to create a secret key route within my app. In the .env file this looks like:

AUTODEPLOY_SECRET=BHBfCiC0bjIDCAGH2I54JACwKNrC2dqn
AUTODEPLOY_ROUTE=UG2Yu8QzHY6KbxvLNxcRs0HVy9lQnKsx

The config for this package creates two keys, auto-deploy.secret and auto-deploy.route that I can access when registering the route so that it never gets published in any repo:

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

I can then go to Github and register my webook like this:

In this way, both the deployment URL and the key used to authenticate the request will remain secret, and prevent a malicious agent from triggering random deployments on the site.

Global Middleware for Authenticating Webhook Requests

The next part of the approach involves creating a piece of global middleware for the Laravel app that would catch and authenticate the webhook requests. I am able to make sure that my middleware gets executed near the beginning of the queue by using an approach demonstrated in this Laracasts discussion thread. In the ServiceProvider for my package, I can prepend a new global middleware class as follows:

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 looks like:

Route::post(
    config('auto-deploy.route'), [
        'as' => 'autodeployroute',
        'uses' => 'MyPackage\AutoDeploy\Controllers\DeployController@index',
    ]
);

And then my middleware would implement a handle() method that looks something like:

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);
}

This function works right up until the continue on to controller bit.

The Problem in Detail

Of course the problem here is that since this is a POST request, and there is no session() and no way to get a CSRF token in advance, the global VerifyCsrfToken middleware generates a TokenMismatchException and aborts. I have read through numerous forum threads, and dug through the source code, but I can't find any clean and easy way to disable the VerifyCsrfToken middleware for this one request. I have tried several workarounds, but I don't like them for various reasons.

Workaround Attempt #1: Have user modify VerifyCsrfToken middleware

The documented and supported method for solving this problem is to add the URL to the $except array in the App\Http\Middleware\VerifyCsrfToken class, e.g.

// The URIs that should be excluded from CSRF verification
protected $except = [
    'UG2Yu8QzHY6KbxvLNxcRs0HVy9lQnKsx',
];

The problem with this, obviously, is that when this code gets checked into the repo, it will be visible to anyone who happens to look. To get around this I tried:

protected $except = [
    config('auto-deploy.route'),
];

But PHP didn't like it. I also tried using the route name here:

protected $except = [
    'autodeployroute',
];

But this doesn't work either. It has to be the actual URL. The thing that actually does work is to override the constructor:

protected $except = [];

public function __construct(\Illuminate\Contracts\Encryption\Encrypter $encrypter)
{
    parent::__construct($encrypter);
    $this->except[] = config('auto-deploy.route');
}

But this would have to be part of the installation instructions, and would be an unusual install step for a Laravel package. I have a feeling this is the solution I'll end up adopting, as I guess it's not really that difficult to ask users to do this. And it has the upside of at least possibly making them conscious that the package they're about to install circumvents some of Laravel's built in security.

Workaround Attempt #2: catch the TokenMismatchException

The next thing I tried was to see if I could just catch the exception, then ignore it and move on, i.e.:

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);
}

Yeah, go ahead and laugh at me now. Silly wabbit, that's not how try/catch works! Of course $response is undefined within the catch block. And If I try doing $next($request) in the catch block, it just bangs up against the TokenMismatchException again.

Workaround Attempt #3: Run ALL of my code in the middleware

Of course, I could just forget about using a Controller for the deploy logic and trigger everything from the middleware's handle() method. The request lifecycle would end there, and I would never let the rest of the middleware propagate. I can't help feeling that there's something inelegant about that, and that it departs from the overall design patterns upon which Laravel is built so much that it would end up making maintenance and collaboration difficult moving forward. At least I know it would work.

Workaround Attempt #4: Modify the Pipeline

Philip Brown has an excellent tutorial describing the Pipeline pattern and how it gets implemented in Laravel. Laravel's middleware uses this pattern. I thought maybe, just maybe, there was a way to get access to the Pipeline object that queues up the middleware packages, loop through them, and remove the CSRF one for my route. Best I can tell, there are ways to add new elements to the pipeline, but no way to find out what's in it or to modify it in any way. If you know of a way, please let me know!!!

Workaround Attempt #5: Use the WithoutMiddleware trait

I haven't investigated this one quite as thoroughly, yet, but it appears that this trait was added recently to allow testing routes without having to worry about middleware. It's clearly NOT meant for production, and disabling the middleware would mean that I'd have to come up with a whole new solution for figuring out how to get my package to do its thing. I decided this was not the way to go.

Workaround Attempt #6: Give up. Just use Forge or Envoyer

Why reinvent the wheel? Why not just pay for one or both of these service that already supports push-to-deploy rather than go to the trouble of rolling my own package? Well, for one, I only pay $5/month for my server, so somehow the economics of paying another $5 or $10 per month for one of these services doesn't feel right. I'm a teacher who builds apps to support my teaching. None of them generate revenue, and although I could probably afford it, this kinda thing adds up over time.

Discussion

Okay, so I've spent the better part of two solid days banging my head against this problem, which is what brought me here looking for help. Do you have a solution? If you've read this far, perhaps you'll indulge a couple of closing thoughts.

Thought #1: Bravo to the Laravel guys for taking security seriously!

I'm really impressed with how difficult it is to write a package that circumvents the built-in security mechanisms. I'm not talking about "circumvention" in the I'm-trying-to-do-something-bad way, but in the sense that I'm trying to write a legitimate package that would save me and lots of other people time, but would, in effect, be asking them to "trust me" with the security of their applications by potentially opening them up to malicious deployment triggers. This should be tough to get right, and it is.

Thought #2: Maybe I shouldn't be doing this

Frequently if something is hard or impossible to implement in code, that is by design. Maybe it's Bad Design™ on my part to want to automate the entire installation process for this package. Maybe this is the code telling me, "Don't do that!" What do you think?

In summary, here are two questions:

  1. Do you know a way to do this that I haven't thought of?
  2. Is this bad design? Should I not do it?

Thanks for reading, and thank you for your thoughtful answers.

P.S. Before someone says it, I know this might be a duplicate, but I provided much more detail than the other poster, and he never found a solution, either.

解决方案

I know it is not good practice to use the Reflection API in production code, but this is the only solution i could think of where no additional configuration is needed. This is more like a proof of concept and I would not use it in production code.

I think a better and more stable solution is to have the user update his middleware to work with your package.

tl;dr - you can place this in your packages boot code:

// 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软件包中添加来自CSRF检查的异常的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆