HTTP status code · Client errors (4xx)
403 Forbidden
403 Forbidden means the server understood the request and refuses to fulfill it, whoever you are. The most common causes are file or directory permissions on the web server, a missing index file with directory listing disabled, or a firewall rule (WAF, IP block, bot protection) rejecting the request.
| Class | 4xx, Client errors |
|---|---|
| Defined in | RFC 9110 §15.5.4 |
| Cacheable by default | Only with explicit Cache-Control or Expires |
| Safe to retry | Not with the same credentials; only with different ones or after permissions change |
| Relevant headers | None specific to this code |
What 403 means
RFC 9110, section 15.5.4, says the server understood the request but refuses to fulfill it. If credentials were sent, the server considers them insufficient, and the client should not repeat the request with the same credentials. The refusal may also have nothing to do with credentials: an IP ban, a country block or a rule about the URL itself all produce 403.
The same section allows an origin server to answer 404 instead when it wants to hide that a forbidden resource exists. That is why a private GitHub repository or an S3 object you cannot list may show up as 404 or 403 depending on the service and your permissions.
Many 403s never reach the application. nginx returns 403 when a request maps to a directory without an index file and autoindex is off, or when the worker process cannot read a file. Cloudflare, AWS WAF and similar layers return 403 for requests that look automated, which is why a URL can work in the browser and fail from curl or a script.
Common causes
If you are visiting the site
- The page is restricted to certain accounts, roles or subscribers, and yours is not one of them.
- Your IP address, country or VPN exit node is blocked by the site or its firewall.
- Bot protection flagged the request: an unusual browser, disabled JavaScript or a script without browser headers.
If you run the server
- File permissions: the web server user (www-data, nginx) cannot read the file or traverse a parent directory. Typical safe values are 644 for files and 755 for directories.
- A directory URL with no index.html or index.php and directory listing turned off (nginx: "directory index of ... is forbidden" in error.log).
- Explicit deny rules: nginx deny, Apache Require all denied, a .htaccess rule, or a WordPress security plugin blocking the path or your IP.
- Amazon S3 returns 403 AccessDenied when the bucket policy or IAM role does not allow the action, and also for missing objects when the caller lacks s3:ListBucket.
- Framework guards: a failed CSRF check (Django answers 403 "CSRF verification failed"), or a role check in your own code.
How to fix it
If you are visiting the site
- Make sure you are logged in with the account that should have access; ask the owner to grant it if not.
- Turn off the VPN or proxy and reload, in case the exit IP is blocked.
- Clear the site cookies and cache, then try another browser to rule out a flagged session.
If you run the server
- Read the web server error log first; nginx and Apache log the exact reason (permission denied, directory index forbidden, access forbidden by rule).
- Fix ownership and modes: files 644, directories 755, owned by the deploy user and readable by the server user. Check every parent directory, not just the file.
- Add the index file or set the right index directive, and only enable autoindex if you want a public listing.
- For S3, check the bucket policy, Block Public Access settings and the IAM policy of the caller; grant s3:ListBucket if you want real 404s for missing keys.
- In Cloudflare or another WAF, open the security events log, find the rule that fired for the request, and add an exception if it is legitimate traffic.
How to send 403
app.delete('/projects/:id', (req, res) => {
res.status(403).json({ error: 'Only the project owner can delete it' });
});// app/projects/[id]/route.ts
export async function DELETE() {
return Response.json(
{ error: 'Only the project owner can delete it' },
{ status: 403 }
);
}mux.HandleFunc("DELETE /projects/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden) // 403
w.Write([]byte(`{"error":"Only the project owner can delete it"}`))
})from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.delete("/projects/{id}")
def delete_project(id: str):
raise HTTPException(status_code=403, detail="Only the project owner can delete it")# Both blocks answer 403 Forbidden
# Never serve the .git folder or dotfiles
location ~ /\. {
deny all;
}
# Allow the admin area from the office network only
location /admin/ {
allow 203.0.113.0/24;
deny all;
}Commonly confused with
- 403 vs 401
- 401 asks the client to authenticate and a login can fix it; 403 refuses regardless, and re-sending the same credentials will not help.
- 403 vs 404
- Servers may answer 404 instead of 403 to avoid confirming that a forbidden resource exists.
- 403 vs 451
- 451 is a refusal for legal reasons, such as a court order or geoblocking required by law; 403 gives no reason.
Frequently asked questions
- What is the difference between 401 and 403?
- 401 means the server needs you to authenticate, and valid credentials may fix it. 403 means the server refuses even though it understood the request, so logging in again with the same account does not help.
- Why do I get 403 with curl or Python but not in the browser?
- A WAF or bot protection (Cloudflare, Akamai, AWS WAF) is rejecting requests that do not look like a browser: a default user agent, missing Accept headers or a non-browser TLS fingerprint. Check whether the site offers an API, and respect its terms before trying to look like a browser.
- How do I fix 403 Forbidden on nginx?
- Check /var/log/nginx/error.log. "directory index of ... is forbidden" means there is no index file for that folder; "permission denied" means the nginx user cannot read the file or enter a parent directory. Fix with chmod 644 on files, 755 on directories, or add the index file.
- Why does WordPress show 403 Forbidden?
- Most often a security plugin or a .htaccess rule blocking the request, wrong file permissions after a migration, or the host firewall (ModSecurity) flagging a form submission. Temporarily renaming the plugins folder over SFTP quickly shows whether a plugin is responsible.
- Why does S3 return 403 for a file that does not exist?
- Without the s3:ListBucket permission, S3 will not tell you whether a key exists, so a missing object returns 403 AccessDenied instead of 404. Grant ListBucket to the caller to get real 404s.
Last reviewed by Arielton Oberek.