# HTTP 403 Forbidden

**Code:** `403` · **Category:** HTTP · **Seen on:** Apache, Nginx, PHP, WordPress, REST API, Cloud

The server understood the request but refuses to authorize it — you (or the server process) are not allowed to access this resource.

## Meaning

A 403 means access is denied on purpose. Unlike 401, logging in again usually won’t help: either your account lacks permission, or the server itself is configured to refuse the request (file permissions, IP rules, a WAF, a missing index file).

On web servers the most common sources are file-system permissions, directory listing being disabled with no index file, and deny rules. On APIs it usually means your token is valid but lacks the required scope or role.

## Common causes

- The authenticated user/token lacks the required role, scope or permission
- File or directory permissions don’t let the web server user (`www-data`, `nginx`, `apache`) read the file
- Directory requested with no `index.html`/`index.php` and directory listing disabled
- `deny` / `Require all denied` rules, IP allow-lists or geo-blocking
- Firewall / WAF (Cloudflare, ModSecurity, AWS WAF) blocking the request pattern or user agent
- CSRF token missing or invalid on a form POST (many frameworks answer 403)
- Hotlink protection or a missing/blocked Referer
- SELinux context preventing the web server from reading files

## Quick fix

1. Check the server error log — it usually says exactly why (permissions, index, rule)
2. Make files readable by the web server: `chmod 644` files, `chmod 755` directories
3. Add an index file or the right `index` / `DirectoryIndex` directive
4. Remove or adjust the deny / IP rule that matches your request
5. For APIs: confirm the token has the needed scope/role, not just that it’s valid
6. Check WAF / Cloudflare firewall events for a blocked request

## Detailed fix by platform

### Nginx

1. Look in `/var/log/nginx/error.log` for "permission denied", "directory index … is forbidden" or "access forbidden by rule".
2. Ensure an index exists and the root is readable:

```nginx
server {
    root /var/www/app/public;
    index index.php index.html;
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
}
```

3. Fix ownership: `sudo chown -R www-data:www-data /var/www/app` (user may be `nginx` on RHEL).
4. Every parent directory needs execute (`x`) permission for the nginx user.

### Apache

1. Check `/var/log/apache2/error.log` (or `httpd/error_log`) for "AH01630: client denied by server configuration" or "AH01276: Cannot serve directory".
2. Allow access in the vhost or .htaccess:

```apache
<Directory /var/www/app/public>
    Options -Indexes +FollowSymLinks
    AllowOverride All
    Require all granted
</Directory>
DirectoryIndex index.php index.html
```

3. Search `.htaccess` files for `Deny from`, `Require ip` or `RewriteRule … [F]`.

### PHP

1. If your app returns 403 itself, log the authorization decision (user id, role, required permission).
2. For CSRF-protected forms, make sure the token is rendered in the form and the session cookie is sent.

### WordPress

1. Temporarily rename the `plugins` folder to rule out security plugins (Wordfence, iThemes).
2. Regenerate `.htaccess` by re-saving Settings → Permalinks.
3. Reset permissions: directories 755, files 644, `wp-config.php` 640.

### REST API

1. Decode the token and verify `scope`/`roles` include what the endpoint needs.
2. Check the API’s IP allow-list and per-key restrictions (allowed referrers, apps).
3. Return 401 for bad/missing credentials and 403 only for valid-but-insufficient ones.

### Cloud

1. AWS S3/CloudFront: check bucket policy, Block Public Access and Origin Access Control (S3 returns 403 for missing objects when you lack ListBucket).
2. Cloudflare: Security → Events shows which WAF rule blocked the request.

## Code examples

### Reproduce and see the reason

```bash
curl -I https://example.com/api/users
sudo tail -n 50 /var/log/nginx/error.log
namei -l /var/www/app/public/index.php   # shows permissions of every path segment
```

`namei -l` quickly reveals a parent directory the web server can’t traverse.

### Handle 403 differently from 401 in the client

```javascript
const res = await fetch('/api/data', { headers: { Authorization: `Bearer ${token}` } });
if (res.status === 401) return redirectToLogin();       // not authenticated
if (res.status === 403) return showError('You do not have access to this data.'); // authenticated but not allowed
```

## How to diagnose

1. **Request** — Correct URL, method and host? Any unusual headers or user agent?
2. **Authentication** — Is the user/token recognized at all? (If not, expect 401.)
3. **Authorization** — Does this user/token have the role or scope for this resource?
4. **Server** — File permissions, index file, deny rules — check the server error log
5. **Firewall** — WAF, CDN or IP rules blocking the request?
6. **Application** — Does app code (CSRF, middleware, policies) return 403?

---
Source: [WhatIsMyError](https://whatismyerror.com/errors/http/403-forbidden)
