Introduction
When building a web application, you typically add authorisation checks to ensure that users can only access resources they are permitted to. For example, on a blogging platform, you'd want to ensure that users can only edit or delete their own posts, and not the posts of other users.
If a user tries to access a resource they aren't authorised to, you'd typically return an HTTP 403 response, which pretty much means "Go away! You're not allowed to do that!".
But in this article, we're going to discuss the idea of returning an HTTP 404 response in these situations instead. We'll also look at how to implement this in a Laravel application, and some of the things you should consider before doing so.
Returning 404 for Unauthorised Access
Typically, when you return an HTTP 403 response, you're indicating that the resource exists, but the user isn't authorised to access it. The downside to this is that if someone is maliciously trying to probe your application for resources, returning a 403 can inadvertently confirm the existence of that resource.
From here, the attacker will know that the resource exists and can build out a list of valid resources to target. Your application should be locked down anyway, but if the attacker finds a vulnerability, this list of valid resources can be used by the attacker as a set of targets.
So, as we've mentioned, you might want to consider returning an HTTP 404 response instead of an HTTP 403 for unauthorised access. By doing this, you're not confirming whether the resource exists or not. This will lead to 3 possible outcomes if someone tries to access a resource and receives an HTTP 404 response:
- The route itself in your application doesn't exist.
- The resource exists, but the user isn't authorised to access it.
- The resource doesn't exist at all.
As a result, it makes it much harder (but not impossible, but we'll get to that later) for an attacker to build out a list of valid resources in your application. When they receive a 404, they'll have no idea whether the resource exists or not.
Laravel Example
For any Laravel developers reading this article, let's look at an example of how you might implement this in your application.
Imagine we have an App\Models\Post model that belongs to a user. The user can only update the post if they are the owner of it. Your controller method might look something like this:
         1declare(strict_types=1); 2  3namespace App\Http\Controllers; 4  5use App\Models\Post; 6use Illuminate\Http\RedirectResponse; 7use Illuminate\Http\Request; 8use Illuminate\Support\Facades\Gate; 9 10final class PostController extends Controller11{12    /**13     * Update the given post.14     */15    public function update(Request $request, Post $post): RedirectResponse16    {17        // Authorise whether the user can update the post.18        Gate::authorize('update', $post);19 20        // Update the post here...21 22        return redirect('/posts');23    }24}    
Then your policy might look something like this:
         1declare(strict_types=1); 2  3namespace App\Policies; 4  5use App\Models\Post; 6use App\Models\User; 7  8final readonly class PostPolicy 9{10    /**11     * Determine if the given post can be updated by the user.12     */13    public function update(User $user, Post $post): bool14    {15        return $user->id === $post->user_id;16    }17}    
If a user tries to update a Post they don't own, the update policy method will return false. This will then result in the Gate::authorize method causing Laravel to return an HTTP 403 response.
However, if you want to return an HTTP 404 instead, you can update your policy like so:
         1declare(strict_types=1); 2  3namespace App\Policies; 4  5use App\Models\Post; 6use App\Models\User; 7use Illuminate\Auth\Access\Response; 8  9final readonly class PostPolicy10{11    /**12     * Determine if the given post can be updated by the user.13     */14    public function update(User $user, Post $post): Response15    {16        return $user->id === $post->user_id17            ? Response::allow()18            : Response::denyAsNotFound();19    }20}    
Now, if a user tries to update a post they don't own, Laravel will return a 404 response instead of a 403.
Things to Consider
Although returning a 404 instead of a 403 can help to obscure the existence of resources in your application, there are some things to consider:
Not Always Needed
Let's imagine you have a web application which has public profiles for users. Each user has a unique username, and their profile can be accessed at a URL like /users/{username}, and updated at /users/{username}/edit.
We'll assume that we know a user exists with the username johndoe because they are an active user on the platform, and we can see their profile at /users/johndoe.
If we navigated to /users/johndoe/edit, and we weren't logged in as johndoe, then it would make sense to return an HTTP 403 response. After all, we know the user exists, and we know that they have a profile that can be edited, but we're simply not authorised to edit it. We don't need to obscure the existence of the resource here, because it's already public knowledge.
Of course, if you want to be extra cautious, maybe you'd still want to return a 404 response to reduce the likelihood of user enumeration attacks. But in most cases, it's not strictly necessary.
For this reason, I think HTTP 404 responses really shine when you're dealing with resources that aren't public knowledge. For example, if you're building an application where users can only access resources that belong to their own account, then returning a 404 response for unauthorised access makes sense. A user should never have knowledge of resources that belong to other users.
Mixing HTTP 404 and 403 Responses
There may also be times when you want to use a mixture of HTTP 403 and 404 responses in your application. For example, imagine you're building a multi-tenant application and that users can belong to a team and are assigned roles.
Let's say a user in Team A tries to access a resource that belongs to Team B. In this case, it would make sense to return a 404 response because the team shouldn't have knowledge of resources that belong to other teams.
However, if a user in Team A tries to access a resource that belongs to Team A, but they don't have the correct role to access it, then it would make sense to return a 403 response. After all, they know the resource exists because it belongs to their team, but they simply aren't authorised to access it.
Harder Debugging
When you're building the application or debugging issues, returning a 404 for both non-existent resources and unauthorised access can make it harder to identify the root cause of an issue. After all, we typically use HTTP status codes to help us understand what went wrong.
User Experience
Although in most cases, you'd never present the user with a link that returns a 403 response, if you do, then returning an HTTP 404 error page instead of an HTTP 403 error page might confuse the user. But this is a minor concern in most cases, and something the user is unlikely to encounter.
Timing Attacks
Returning an HTTP 404 status code instead of a 403 might help to obscure the existence of resources, but it doesn't completely hide them. A timing attack could still be used to determine whether a resource exists or not.
If you've not heard of timing attacks before, I'd highly recommend checking out Stephen Rees-Carter's "In Depth: Timing Attacks" article that covers them. But the general idea is that an attacker can measure the time it takes for your application to respond to a request to determine whether a resource exists or not.
At a high level, think about the steps your application might take to process a request when attempting to access a resource. It needs to make a database query to fetch the resource. If the resource exists, it will then hydrate the row into a model (assuming you're using model classes), and then check whether the user is authorised to access it. If the resource doesn't exist, it will be able to skip the hydration and authorisation check steps. This means if the resource exists, the request will take slightly longer to process than if the resource doesn't exist.
With enough requests to build up a baseline, an attacker could potentially determine the average response time for a request for an existing resource versus a non-existent resource. This could allow them to infer whether a resource exists or not, even if you're returning a 404 for both cases.
There are things you can do to mitigate timing attacks, such as adding random delays to your responses, but this is a complex topic and beyond the scope of this article.
But the main takeaway is that returning a 404 instead of a 403 can help to obscure the existence of resources, but it doesn't completely hide them.
Conclusion
Hopefully, this article has given you some food for thought about whether you should return an HTTP 404 response instead of a 403 for unauthorised access in your application.
If you enjoyed reading this post, you might be interested in checking out my 220+ page ebook "Battle Ready Laravel" which covers similar topics in more depth.
Or, you might want to check out my other 440+ page ebook "Consuming APIs in Laravel" which teaches you how to use Laravel to consume APIs from other services.
If you're interested in getting updated each time I publish a new post, feel free to sign up for my newsletter below.
Keep on building awesome stuff! 🚀
 
                            