Support the ongoing development of Laravel.io →
Article Hero Image

Returning HTTP 404 Responses Instead of 403 for Unauthorised Access

5 Nov, 2025 10 min read

Photo by Scott Webb on Unsplash

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 sometimes 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.

Before we delve any deeper into this article, I also just want to point out that I'm not advocating for completely replacing HTTP 403 responses in your applications with HTTP 404 responses. Instead, I want to discuss the idea of returning 404s in situations where it makes sense and is suitable for the feature you're building.

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. And in most cases, this is exactly what you want to do. However, 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, in some situations (we'll look at an example later) 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:

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;

final class PostController extends Controller
{
    /**
     * Update the given post.
     */
    public function update(Request $request, Post $post): RedirectResponse
    {
        // Authorise whether the user can update the post.
        Gate::authorize('update', $post);

        // Update the post here...

        return redirect('/posts');
    }
}

Then your policy might look something like this:

declare(strict_types=1);

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

final readonly class PostPolicy
{
    /**
     * Determine if the given post can be updated by the user.
     */
    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }
}

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:

declare(strict_types=1);

namespace App\Policies;

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;

final readonly class PostPolicy
{
    /**
     * Determine if the given post can be updated by the user.
     */
    public function update(User $user, Post $post): Response
    {
        return $user->id === $post->user_id
            ? Response::allow()
            : Response::denyAsNotFound();
    }
}

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.

A great example to highlight this is GitHub private repositories. If you attempt to visit a private repository which you don't have authorisation to view, you'll receive an HTTP 404 response. This is the same response you'd also receive if the repository didn't exist at all. As a result, you can't determine whether the repository exists or not (at least, not based on the response status code anyway). However, if GitHub were to return a 403 response if the repository did exist, then you'd be able to use that as proof that it exists

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.

Use Alongside Other Security Measures

As we've touched on in the article, you shouldn't rely solely on the status code of your response as a security measure to hide the existence of a resource. Instead, you should treat it as one of many tools in your arsenal.

For example, to reduce the effectiveness of an enumeration attack (where an attacker might be looping through example.com/users/1, example.com/users/2, etc., to find which users exist), you might want to use UUIDs, ULIDs, or some other kind of obfuscated ID rather than auto-incrementing IDs. This could change your URL structure to something more like example.com/users/dc3dd10c-24ac-46f2-a603-583cfd8b36e2. Using a less predictable pattern, such as this, will make it much harder to guess the next resource's ID.

You can also implement rate limiting to make it more difficult for an attacker to send a large number of requests within a short period. This will make it significantly harder to use a brute force technique of trying lots of possible URLs.

By partnering HTTP 404 responses, obfuscated IDs, rate limiting, a technique for mitigating timing attacks, and other security measures, you can establish a robust foundation for reducing enumeration attacks.

However, due to some of the drawbacks we've mentioned, you should remember to use HTTP 404 responses only where it makes sense. If you won't gain much value from using the 404 response, then I'd recommend sticking with the general convention and returning a 403 response.

Conclusion

Hopefully, this article has given you some food for thought about whether you should sometimes 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.

Keep on building awesome stuff! 🚀

Last updated 3 hours ago.
0
Like this article? Let the author know and give them a clap!
ash-jc-allen (Ash Allen) I'm a freelance Laravel web developer from Preston, UK. I maintain the Ash Allen Design blog and get to work on loads of cool and exciting projects ?

Other articles you might like

Article Hero Image November 4th 2025

Laravel 12 Custom Validation Rules Example

In this Laravel tutorial titled “laravel 12 custom validation rules example”, you will learn how to...

Read article
Article Hero Image October 29th 2025

Run PHPUnit and Pest Tests Without Vite Assets in Laravel

Introduction A common way to build your Laravel application's frontend assets is with Vite (by runni...

Read article
Article Hero Image October 27th 2025

The Dangers of Dynamic Method Calls in PHP

Introduction In your PHP application, you may sometimes see dynamic method calls being used. This is...

Read article

We'd like to thank these amazing companies for supporting us

Your logo here?

Laravel.io

The Laravel portal for problem solving, knowledge sharing and community building.

© 2025 Laravel.io - All rights reserved.