skip to Main Content

Lets say I have the following route to display a specific user in an API point, that is protected via some authentication middle:

Route::get('/v1/user/{user}', 'ApiV1UserController@show')->middleware('can:show,user');

Assume my database is filled with just a handfull records. Then, going to the route without a credential:

  • /api/v1/user/1 will give me 403 UNAUTHORIZED
  • /api/v1/user/999999 will give me 404 NOT FOUND

In this setup, any visitor can find out how many users I have, by just poking around and trying some routes. Moreover, they can also find out the primary identifier (the id) of these users. This is something I would like to hide from a business perspective, but also from a security perspective.

An appoach that partially addresses this issue, is using UUID’s. UUIDs are universally unique alpha-numeric identifiers that are 36 characters long, and Laravel supports the use of these on your models. This will hide the amount of records our have, and make it hard to find existing records. However, since it is still statistically possible to find records by just brute forcing, I feel this is not the correct answer to this problem.

So, how can I prevent a Laravel security leak where API returns not found when you are not authenticated?

3

Answers


  1. you can use this package to generate a slug.
    Then you can use the slug instead of the id.

    https://github.com/spatie/laravel-sluggable

    Login or Signup to reply.
  2. You can follow GitHub’s technique and return a 404 (Not found) instead of 403 (Unauthorized).

    This way, attackers don’t know if the resource actually exists or not.

    To achieve this in Laravel, you may do it like this:
    In app/Exceptions/Handler.php, create/edit the method called render() and check if the status code is 403. If so, throw a 404 instead.

    public function render($request, Throwable $exception)
    {
        if ($exception->getStatusCode() === 403) {
            abort(404);
        }
    
        return parent::render($request, $exception);
    }
    

    If you want to test it, just add this test route in routes/web.php:

    Route::get('/test', function () {
        return abort(403); // should return a 404
    });
    

    Resources:

    Login or Signup to reply.
  3. // Path: app/Http/Controllers/Api/V1/UserController.php
    <?php
    
    namespace AppHttpControllersApiV1;
    
    use AppHttpControllersController;
    use AppUser;
    use IlluminateHttpRequest;
    
    class UserController extends Controller
    {
        public function show(User $user)
        {
            return $user;
        }
    }
    
    // Path: app/User.php
    <?php
    
    namespace App;
    
    use IlluminateNotificationsNotifiable;
    use IlluminateFoundationAuthUser as Authenticatable;
    
    class User extends Authenticatable
    {
        use Notifiable;
    
        /**
         * The attributes that are mass assignable.
         *
         * @var array
         */
        protected $fillable = [
            'name', 'email', 'password',
        ];
    
        /**
         * The attributes that should be hidden for arrays.
         *
         * @var array
         */
        protected $hidden = [
            'password', 'remember_token',
        ];
    }
    
    // Path: routes/api.php
    <?php
    
    Route::get('/v1/user/{user}', 'ApiV1UserController@show')->middleware('can:show,user');
    
    // Path: app/Providers/AuthServiceProvider.php
    <?php
    
    namespace AppProviders;
    
    use IlluminateFoundationSupportProvidersAuthServiceProvider as ServiceProvider;
    use IlluminateSupportFacadesGate;
    
    class AuthServiceProvider extends ServiceProvider
    {
        /**
         * The policy mappings for the application.
         *
         * @var array
         */
        protected $policies = [
            'AppModel' => 'AppPoliciesModelPolicy',
        ];
    
        /**
         * Register any authentication / authorization services.
         *
         * @return void
         */
        public function boot()
        {
            $this->registerPolicies();
    
            //
        }
    }
    
    // Path: app/Http/Middleware/Can.php
    <?php
    
    namespace AppHttpMiddleware;
    
    use Closure;
    use IlluminateAuthAccessAuthorizationException;
    use IlluminateAuthAccessResponse;
    use IlluminateSupportFacadesGate;
    
    class Can
    {
        /**
         * Handle an incoming request.
         *
         * @param  IlluminateHttpRequest  $request
         * @param  Closure  $next
         * @param  string  $ability
         * @param  array|string  $arguments
         * @return mixed
         *
         * @throws IlluminateAuthAccessAuthorizationException
         */
        public function handle($request, Closure $next, $ability, ...$arguments)
        {
            if (Gate::denies($ability, $arguments)) {
                throw new AuthorizationException(
                    'This action is unauthorized.', null, Response::deny()
                );
            }
    
            return $next($request);
        }
    }
    
    // Path: app/Http/Kernel.php
    <?php
    
    namespace AppHttp;
    
    use AppHttpMiddlewareCan;
    use IlluminateFoundationHttpKernel as HttpKernel;
    
    class Kernel extends HttpKernel
    {
        /**
         * The application's global HTTP middleware stack.
         *
         * These middleware are run during every request to your application.
         *
         * @var array
         */
        protected $middleware = [
            IlluminateFoundationHttpMiddlewareCheckForMaintenanceMode::class,
        ];
    
        /**
         * The application's route middleware groups.
         *
         * @var array
         */
        protected $middlewareGroups = [
            'web' => [
                AppHttpMiddlewareEncryptCookies::class,
                IlluminateCookieMiddlewareAddQueuedCookiesToResponse::class,
                IlluminateSessionMiddlewareStartSession::class,
                // IlluminateSessionMiddlewareAuthenticateSession::class,
                IlluminateViewMiddlewareShareErrorsFromSession::class,
                AppHttpMiddlewareVerifyCsrfToken::class,
                IlluminateRoutingMiddlewareSubstituteBindings::class,
            ],
    
            'api' => [
                'throttle:60,1',
                'bindings',
            ],
        ];
    
        /**
         * The application's route middleware.
         *
         * These middleware may be assigned to groups or used individually.
         *
         * @var array
         */
        protected $routeMiddleware = [
            'auth' => IlluminateAuthMiddlewareAuthenticate::class,
            'auth.basic' => IlluminateAuthMiddlewareAuthenticateWithBasicAuth::class,
            'bindings' => IlluminateRoutingMiddlewareSubstituteBindings::class,
            'cache.headers' => IlluminateHttpMiddlewareSetCacheHeaders::class,
            'can' => Can::class,
            'guest' => AppHttpMiddlewareRedirectIfAuthenticated::class,
            'signed' => IlluminateRoutingMiddlewareValidateSignature::class,
            'throttle' => IlluminateRoutingMiddlewareThrottleRequests::class,
            'verified' => IlluminateAuthMiddlewareEnsureEmailIsVerified::class,
        ];
    }
    
    // Path: app/Http/Controllers/Api/V1/UserControllerTest.php
    <?php
    
    namespace TestsFeatureApiV1;
    
    use AppUser;
    use IlluminateFoundationTestingRefreshDatabase;
    use IlluminateFoundationTestingWithFaker;
    use TestsTestCase;
    
    class UserControllerTest extends TestCase
    {
        use RefreshDatabase;
    
        public function testShow()
        {
            $user = factory(User::class)->create();
    
            $response = $this->getJson("/api/v1/user/{$user->id}");
    
            $response->assertStatus(200);
        }
    
        public function testShowUnauthorized()
        {
            $user = factory(User::class)->create();
    
            $response = $this->getJson("/api/v1/user/{$user->id}");
    
            $response->assertStatus(403);
        }
    
        public function testShowUnauthorizedWithPolicy()
        {
            $user = factory(User::class)->create();
    
            $response = $this->getJson("/api/v1/user/{$user->id}");
    
            $response->assertStatus(403);
        }
    }
    
    // Path: app/Policies/UserPolicy.php
    <?php
    
    namespace AppPolicies;
    
    use AppUser;
    use IlluminateAuthAccessHandlesAuthorization;
    
    class UserPolicy
    {
        use HandlesAuthorization;
    
        /**
         * Determine whether the user can view the model.
         *
         * @param  AppUser  $user
         * @param  AppUser  $model
         * @return mixed
         */
        public function show(User $user, User $model)
        {
            return $user->id === $model->id;
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search