Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.0] route model binding runing before middleware #6118

Closed
aliuygur opened this issue Oct 16, 2014 · 75 comments
Closed

[5.0] route model binding runing before middleware #6118

aliuygur opened this issue Oct 16, 2014 · 75 comments
Labels

Comments

@aliuygur
Copy link
Contributor

Route model binding runing before middleware, this is bug.

@aliuygur aliuygur changed the title route model binding runing before middleware [5.0][Bug] route model binding runing before middleware Oct 16, 2014
@aliuygur aliuygur changed the title [5.0][Bug] route model binding runing before middleware [5.0] [Bug] route model binding runing before middleware Oct 16, 2014
@GrahamCampbell
Copy link
Member

Pull request to fix?

@aliuygur
Copy link
Contributor Author

@GrahamCampbell, i have no idea that how is it gonna be resolved.

@aliuygur
Copy link
Contributor Author

@taylorotwell this is critical bug. please fix it.

@GrahamCampbell
Copy link
Member

@alioygur This is NOT critical. Any bug that affects laravel 5 is low priority.

DO NOT USE LARAVEL 5 IN PRODUCTION

@taylorotwell
Copy link
Member

Why would this be a bug?

@aliuygur
Copy link
Contributor Author

@taylorotwell, I thinking this is bug. Because,

Imagine you use http basic authentication. (Stateless)

So, user authenticated at middleware (AuthenticatedWithBasic)

And you have a route and model binding, like this

// routes.php

$router->group(["prefix" => "me", "middleware" => "App\Http\Middleware\AuthenticatedWithBasic"], function($router) {

$router->resource("posts", "App\Http\Controllers\PostsController");

});

// routeserviceprovider@before

$router->bind("posts", function($post_id) use ($router){
return $router->user()->posts()->findOrFail($post_id);
});

So, I maybe do somethings with current logged in user order model binding. Currently I can not this because model binding running before middlewares.

I am sorry, for my bad English .
I am on mobile.

@taylorotwell
Copy link
Member

I still don't understand.

On Tuesday, October 21, 2014, ali oygur notifications@github.com wrote:

@taylorotwell https://github.com/Taylorotwell, I thinking this is bug.
Because,

Imagine you use http basic authentication. (Stateless)

So, user authenticated at middleware (AuthenticatedWithBasic)

And you have a route and model binding, like this

// routes.php

$router->group(["prefix" => "me", "middleware" => "App\Http\Middleware\AuthenticatedWithBasic"], function($router) {

$router->resource("posts", "App\Http\Controllers\PostsController");

});

// routeserviceprovider@before

$router->bind("posts", function($post_id) use ($router){
return $router->user()->posts()->findOrFail($post_id);
});

So, I maybe do somethings with current logged in user order model binding.
Currently I can not this because model binding running before middlewares.

I am sorry, for my bad English .
I am on mobile.


Reply to this email directly or view it on GitHub
#6118 (comment).

@aliuygur
Copy link
Contributor Author

okey, just one question

SO, what I supposed to do if I try to get the current logged in user information while binding model.
Note: I am using http basic authentication.

/**
 * Called before routes are registered.
 *
 * Register any model bindings or pattern based filters.
 *
 * @param  Router  $router
 * @return void
 */
public function before(Router $router)
{
    $router->bind('resumes', function ($value, $route) {
        $user = app('auth')->user();
        $model = $user->resumes()->findOrFail($value);

    });
}

I am getting following error.

FatalErrorException in RouteServiceProvider.php line 43:
Error: Call to a member function resumes() on a non-object

because AuthenticatedWithBasic middleware did not run before model binding.

this is AuthenticatedWithBasicAuth file

<?php namespace App\Http\Middleware;

use Closure;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Routing\Middleware;

class AuthenticatedWithBasicAuth implements Middleware {

    /**
     * The Guard implementation.
     *
     * @var Guard
     */
    protected $auth;

    /**
     * Create a new filter instance.
     *
     * @param  Guard  $auth
     * @return void
     */
    public function __construct(Guard $auth)
    {
        $this->auth = $auth;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        return $this->auth->onceBasic() ?: $next($request);
    }

}

@JoostK
Copy link
Contributor

JoostK commented Oct 22, 2014

So basically, in order to find out if the bound object belongs to the authenticated user, you will need access to the current user in the bind callback.

@aliuygur
Copy link
Contributor Author

@JoostK yep! you are right.

@robinmitra
Copy link

I'm in the same boat as @alioygur. Any possible solution to this issue yet? Otherwise, I'm afraid I would have to get rid of all my route model bindings sadly :(

@JoostK
Copy link
Contributor

JoostK commented Dec 28, 2014

This shouldn't have been closed, delaying model binding until after the session data is available makes kind of sense.

@GrahamCampbell GrahamCampbell changed the title [5.0] [Bug] route model binding runing before middleware [5.0] route model binding runing before middleware Dec 28, 2014
@MrAtiebatie
Copy link
Contributor

I came across the same issue. Imagine you have a admin panel where you can manage the users that can login into your website. As admin you want to be able to see users by browsing to admin/users/5 for example. Of course you only want admin's to be able to see that page so you put a middleware in your controller to redirect guests to the login page. In case that user 5 doesn't exist you see a nice 404 not found which is fine to me. BUT, when the guest browses to the admin/users/5 page, he doesn't get redirected to the login page but also sees the 404 not found page which is not fine with me. I hope you can fix this.

@taylorotwell
Copy link
Member

None of that helps me without seeing actual code.

On Thu, Jan 15, 2015 at 4:28 AM, MrAtiebatie notifications@github.com
wrote:

I came across the same issue. Imagine you have a admin panel where you can
manage the users that can login into your website. As admin you want to be
able to see users by browsing to admin/users/5 for example. Of course you
only want admin's to be able to see that page so you put a middleware in
your controller to redirect guests to the login page. In case that user 5
doesn't exist you see a nice 404 not found which is fine to me. BUT, when
the guest browses to the admin/users/5 page, he doesn't get redirected to
the login page but also sees the 404 not found page which is not fine with
me. I hope you can fix this.


Reply to this email directly or view it on GitHub
#6118 (comment).

@MrAtiebatie
Copy link
Contributor

My code or framework code?

@taylorotwell
Copy link
Member

Your code. Show me where the problems are actually happening.

On Thu, Jan 15, 2015 at 9:02 AM, MrAtiebatie notifications@github.com
wrote:

My code or framework code?


Reply to this email directly or view it on GitHub
#6118 (comment).

@MrAtiebatie
Copy link
Contributor

This is my (resource) controller constructor:

/**
 * Constructor
 * @param User $user
 */
function __construct(User $user)
{
    $this->user = $user;

    $this->middleware('guest');
}

This is my guest middleware:

/**
 * Handle an incoming request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Closure  $next
 * @return mixed
 */
public function handle($request, Closure $next)
{
    if ($this->auth->guest())
    {
        return redirect('auth/login');
    }

    return $next($request);
}

And in routes.php:

 Route::resource('admin/users', 'Admin\UsersController');

I have tried the beforeFilter in the constructor and a group filter in routes.php but both still gave a 404 not found, browsing as guest.

@robinmitra
Copy link

Just to chime in, I was roughly trying to do something like the following, but it fails due to this issue:

Route model (or repository in my case) bindings in AppServiceProvider::register() were something like:

public function register()
{
    $repoAbstract = 'App\Contracts\Repositories\Order';
    $router->bind('order', function ($value) use ($repoAbstract) {
        $repo = App::make($repoAbstract);
        if ($model = $repo->find($value)) {
            return $repo;
        }

        throw new NotFoundHttpException('Repository ' . $repoAbstract . ' could not find the model!');
    });
}

And this is Order repository's find() method:

public function find($id)
{
    $model = $this->model
        ->where('id', $id)
        ->where('user_id', Auth::id())
        ->first();

    if ($model) {
        $this->setModel($model);
    }

    return $model;
}

The find() method in the repository expects the user to be authenticated, in order to only return orders that belong to that user, which is pretty standard stuff.

However, using route model (or repository in my case) bindings will fail for the above scenario, since the user hasn't yet been authenticated, which will cause the repository's find() method to fail.

As it stands right now, although I think route model bindings are pretty cool, I've had to disable all of them sadly, since they won't work for my scenario which is quite common frankly.

@landons
Copy link

landons commented Feb 20, 2015

I just ran into a similar issue, but it was pretty easy to get around with dependency injection:

$router->bind('project', function($id)
{
    App::bind('App\Contracts\Project', function() use($id)
    {
        if (($project = Project::findByClient(Auth::user(), $id))) {
            return $project;
        }

        throw new NotFoundHttpException;
    });

    return $id;
});

@GrahamCampbell
Copy link
Member

This is not a bug, but a feature. I was talking to Taylor about this earlier today, not in relation to this though.

@vtalbot
Copy link

vtalbot commented Feb 20, 2015

Could there be a third arguments to bind that defer the binding after the middlewares or an arguments to tell the binding to be done after a middleware or an array of middlewares?

@jordandobrev
Copy link

There is an easy fix for those who are stuck with this issue. Instead of using the bind method in the service provider, just replace the route parameter with the object using middleware.

public function handle($request, Closure $next)
{
    // some middleware logic ....
    if ($request->route()->hasParameter('user')) {
        $user = $this->app->make(UserRepository::class)->getByIdOrFail($request->route()->parameter('user'));
        $request->route()->setParameter('user', $user);
    }

    return $next($request);
}

Cheers ;)

@zot24
Copy link

zot24 commented Mar 30, 2015

Thanks @LPMadness good example!

@GrahamCampbell
Copy link
Member

Doing your own route binding from a middleware isn't that bad tbh. Infact, I quite like that idea, and might even write one and publish it as a package. :)

@lanort
Copy link
Contributor

lanort commented Jan 5, 2016

Maybe this is not the smartest way but I just wrapped my bindings in the RouteServiceProvider.php in an if clause (App uses both: the check here and in the middleware)

if (Auth::check()) {
    // move the bindings here
}

EDIT: DOES NOT WORK. Oh gosh, does not work because Auth::check() will always return false at this stage.

@soee
Copy link

soee commented Jan 21, 2016

I think i have the same problem. Using model binding makes my global scope fail as i need to get in this scope authenticated user ID but it is always NULL in that case. So Laravel throws Trying to get property of non-object when calling Auth::user()->id in my global scope.

@websanova
Copy link
Contributor

+1 for this fix. Auth should come before the route model binding. In my case I'm using JWT for auth, so definitely need the auth check first before route check...

In my case the api exposes certain parameters from a model based on privilege levels. So for instance a users profile only exposes an id and name to general public. But for the admins it exposes all fields.

@amitailanciano
Copy link

+1 since route binding comes first, the authenticated user doesn't exist in my user guard.

I feel like this is a pretty core use case -- @taylorotwell can you comment on why this issue was closed?

EDIT: Just read this: #6118 (comment) however perhaps there ought to be some consideration into how this was implemented in the first place given that so many people encounter this use case.

It'd be great to see this functionality as part of the core route model binding feature, however it gets implemented.

Route::bind('event', function ($value, $route) {
    $event = EntityManager::getRepository(\App\Entities\Event::class)->findOneBy([
        'user' => auth('user')->user(),
        'id' => $value
    ]);

    if (!$event) {
        throw new NotFoundHttpException('Event entity not found');
    }

    return $event;
});

@malhal
Copy link
Contributor

malhal commented Apr 29, 2016

Another month gone by, anyone attempted to fix this show stopper?

@aliuygur
Copy link
Contributor Author

Laravel is Taylor's personnel project not an open source protect.
On Apr 30, 2016 12:39 AM, "Malcolm Hall" notifications@github.com wrote:

Another month gone by, anyone attempted to fix this show stopper?


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
#6118 (comment)

@aliuygur
Copy link
Contributor Author

@rephole ben Go diline geçtim. Artık sikimde değil.

@neyaoz
Copy link
Contributor

neyaoz commented Apr 29, 2016

@alioygur Pire için yorgan yakılmaz, belki ölür bugün yarın.

@simondubois
Copy link

This blog post might help : https://murze.be/2016/05/using-current-user-route-model-binding/
Thanks @freekmurze

@stevebauman
Copy link
Contributor

Just make sure the bindings or \Illuminate\Routing\Middleware\SubstituteBindings middleware is added after your authentication middleware.

@poisa
Copy link
Contributor

poisa commented Nov 6, 2017

I have another use case for this in multi-tenant sites. Let's say your model connects to your tenant db connection. This connection does not exist in the config file because it is dynamic; it depends on who is making the request. I need to authenticate the user and see which database tenant actually refers to. If model binding runs before my middleware, Laravel has no way to know which database to connect to.

@stevebauman's solution is quite right, alas it adds a bit of overhead in my particular design. But in my case, I removed the bindings middleware from all the groups, and added it manually to any route groups where I need model binding. It's more verbose that having this done by the framework, but still perfectly possible to achieve.

@devinfd
Copy link
Contributor

devinfd commented Jan 21, 2018

This problem just bit me. I just learned that a user/attacker of my app was hitting an endpoint to see if records existed. Basically he was phishing the database. It wasn't really a DDOS attack because he was phishing each record manually so each request looked like normal. Long story short, once he learned which records did not exist he took that info to somebody else to complain about their work. I learned of this phishing from that third person that was concerned about how the attacker new which records were deleted. I'll spare you the details of why those records were deleted because the real issue seems to me that an UNAUTHENTICATED user was able to phish my database. THAT IS A SECURITY VULNERABILITY.

What is the actual reason that authentication does/can not happen before route model binding?

@GuidoHendriks
Copy link
Contributor

GuidoHendriks commented Jan 22, 2018

@devinfd I think this issue has been resolved by now. Can't reproduce the behaviour you're talking about. You could try creating a new project and see if it still has this behaviour. If you find out it doesn't, it's probably in the configuration of your project.

If you believe you discovered a security vulnerability, you also shouldn't discuss it on Github, but email Taylor instead: https://laravel.com/docs/5.5/contributions#security-vulnerabilities

@devinfd
Copy link
Contributor

devinfd commented Jan 22, 2018

@GuidoHendrik Thanks. My app is 5.5 and the bug does exists however I tried it on a fresh install of 5.5 and the bug did not exists. Long story short I discovered that the my app was using the old auth middleware 'auth' => \App\Http\Middleware\Authenticate::class. This was updated to 'auth' => \Illuminate\Auth\Middleware\Authenticate::class in 5.3 and I didn't catch that change. All is good now.

@garygreen
Copy link
Contributor

garygreen commented Jan 26, 2018

@JosephSilber @taylorotwell I completely disagree that this use case should just be ignored - it's one of my real niggles with the framework at the moment; middlewares are fired after route model binding. This means that if you have a multi tenant application, or language-driven application, or global scopes, etc then your out of luck using the default built in route model binding:

$router->bind('account', function($id) {
   return Account::where('tenant_id', session('tenant_id'))->findOrFail();
});

Broke.

I can understand why the middlewares are fired after route model binding, but I don't believe it's outside the scope of the application to at least provide functionality to bind AFTER middlewares have fired. Something like:

Route::bindAfterMiddleware(function() {
   Route::bind('product', function($slug) {
      return Product::where('lang', session('lang_code', 'us'))->where('slug', $slug)->firstOrFail();
   });
});

Or for implicit route binding, hint which parameters should be bound after middlewares:

Route::bindAfterMiddleware('product');

Or for binding after session middleware only:

Route::bindAfterMiddleware('product', 'Illuminate\Session\Middleware\StartSession');

Something like that.

There really ought to be a way to cater for this without resorting to creating your own middleware to replace parameters in the url etc, otherwise it renders the whole route model binding feature quite useless in those cases. This would also help with any DDoS driven attacks based on urls as you could tell it to bind routes models AFTER throttler has run, etc.

Ref my suggestion: laravel/ideas/issues/508

@malhal
Copy link
Contributor

malhal commented Jan 26, 2018

Loading in the models via binding before checking auth in middleware reminds me of Spectre!

@garygreen
Copy link
Contributor

garygreen commented Jan 26, 2018

Loading in the models via binding before checking auth in middleware reminds me of Spectre!

Yah, it's definitely not ideal and I can totally understand the DDoS argument. Even if the throtter in Laravel would deny the request, your models have still already loaded and your more susceptible to a DDoS driven style attack on the database.

Like you said though, I wouldn't use that as an argument to fix this, though a fix would likely help with the DDoS issue too.

@devinfd
Copy link
Contributor

devinfd commented Jan 26, 2018

@garygreen what Laravel version are you using? As I understand it, this was fixed in 5.3 however in order to receive that fix you must update the App\Http\Kernall $routeMiddleware auth key to use 'auth' => \Illuminate\Auth\Middleware\Authenticate::class

@garygreen
Copy link
Contributor

garygreen commented Jan 26, 2018

@devinfd I think we're talking about different issues, this is nothing to do with auth. Route model binding is always fired before middlwares, even in Laravel 5.5.

@garygreen
Copy link
Contributor

Nevermind, ignore me. I think since \Illuminate\Routing\Middleware\SubstituteBindings::class was added it just needs tweaking to be registered straight after session is initialised.

@jpuck
Copy link
Contributor

jpuck commented Feb 15, 2018

Set Middleware Priority in App\Http\Kernel

For example, here I need my custom auth middleware to run first (before substitute bindings), so I unshift it onto the stack:

public function __construct(Application $app, Router $router)
{
    /**
     * Because we are using a custom authentication middleware,
     * we want to ensure it's executed early in the stack.
     */
    array_unshift($this->middlewarePriority, MyCustomApiAuthMiddleware::class);

    parent::__construct($app, $router);
}

Alternatively, you could override that entire priority structure if you needed explicit control (not recommended because you'll have to pay closer attention during upgrades to see if the framework changes). Specific to this issue is the SubstituteBindings class that handles route model binding, so just make sure your auth middleware comes sometime before that.

/**
 * The priority-sorted list of middleware.
 *
 * Forces the listed middleware to always be in the given order.
 *
 * @var array
 */
protected $middlewarePriority = [
    \App\Http\Middleware\MyCustomApiAuthMiddleware::class
    \Illuminate\Session\Middleware\StartSession::class,
    \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    \Illuminate\Auth\Middleware\Authenticate::class,
    \Illuminate\Session\Middleware\AuthenticateSession::class,
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
    \Illuminate\Auth\Middleware\Authorize::class,
];

@sebastiaanluca
Copy link
Contributor

I rarely use(d) route binding, but for a new project I'm using https://github.com/Propaganistas/Laravel-FakeId which auto-resolves models from their fake ID and that kind of requires route binding. Of course, that's when I ran into this issue. Wanted to apply global scopes to those models before they were resolved, but Laravel does it the other way around. I can understand from a certain point of view, you want consistency. But sometimes that's a no-go security and usability-wise.

Here's my attempt at fixing the issue: https://gist.github.com/sebastiaanluca/d61e9e2bc874615f82cfd679dee8edce#gistcomment-2358542

Not a 100% covered and tested with this, but feedback welcome!

@denifelixe
Copy link

denifelixe commented Mar 12, 2019

Good news come with laravel 5.3 above
as https://laravel.com/docs/5.3/upgrade said in Binding Substitution Middleware section

Route model binding is now accomplished using middleware. All applications should add the Illuminate\Routing\Middleware\SubstituteBindings to your web middleware group in your app/Http/Kernel.php file

So now, the Route Model Binding is handled by Illuminate\Routing\Middleware\SubstituteBindings in web middleware group.

Since the web middleware group always runs first, you can comment/remove this middleware in web middleware group and add it to routeMiddleware

protected $middlewareGroups = [
        'web' => [
                ...
                ...
                // \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
        ...
        ...
];

protected $routeMiddleware = [
        ...
        ...
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ...
];

after doing that, you can add the 'bindings' middleware in specific order in your route.

e.g

Route::middleware(['auth', 'bindings'])->group(function () {

	Route::get('/', 'Home\HomeController@index');

});

in that example, 'auth' middleware run first before 'bindings' middleware.

but, in below example, 'bindings' middleware run first before 'auth' middleware :

Route::middleware(['bindings', 'auth'])->group(function () {

	Route::get('/', 'Home\HomeController@index');

});

Hope it helps 👍

@Stone624
Copy link

Stone624 commented Dec 3, 2020

Adding a small tip to denifelixe's solution for this issue , I have a Route Group for Auth, Then Account Scopes (which sets scope for all models for the specific account type ie: only access owned resources, a custom middleware) , Then bindings :
Route::middleware(['auth','account_type_scope','bindings'])->group(function(){ // Routes Here // });

This is nice because the vast majority of my routes are within this group, And you can just add another group with just binding middleware bindings surrounding all other public routes as well.

If you have an existing and complex routes structure (where this isn't necessarily easy to refactor or possible), Try adding the middleware Construct Method of every controller, which did solve this issue for me as well. :

ModelController Extends Controller {
    public function __construct(){ $this->middleware('bindings'); }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests