httpseverity: can-fix
403

HTTP 403 Forbidden — What It Means and How to Fix It

403 Forbidden

90% fixable~15 mindifficulty: intermediate

Verified against RFC 9110 §15.5.4 (HTTP Semantics), AWS IAM Policy Evaluation Logic, MDN Web Docs: 403 Forbidden · Updated June 2026

> quick_fix

The server recognized your credentials but your account lacks permission for this resource. Check that your role or API key has the required permissions, the resource exists and belongs to your account, and no IP allowlist or CORS policy is blocking the request.

# For AWS API calls — check what IAM permissions are missing
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789:user/myuser \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::my-bucket/object.txt

What causes this error

HTTP 403 fires when the server is confident in your identity (authentication passed) but your account, role, or token lacks the required permissions. Common causes: insufficient RBAC roles, missing IAM policy statement, resource-level ACL denial, IP allowlist restriction, CORS policy blocking origin, trying to access another user's resource (IDOR check), and directory traversal protection on file servers.

> advertisementAdSense placeholder

How to fix it

  1. 01

    step 1

    Confirm you are authenticated (vs. getting 403 for missing auth)

    Some servers return 403 instead of 401 for missing authentication (especially file servers and older APIs). Check if adding valid credentials changes the response. If yes, the issue is auth, not authorization.

    # Test with credentials vs without
    curl -si https://api.example.com/admin        # without token → may give 403
    curl -si https://api.example.com/admin \
      -H 'Authorization: Bearer your_token'       # with token → if still 403, it's RBAC
  2. 02

    step 2

    Check your role or permission set

    Most APIs use RBAC (role-based access control). Confirm your account has the role that grants access to this endpoint. Check the docs — admin endpoints often require an `admin` or `superuser` role that isn't granted to standard API keys.

    # Check your current permissions via the API
    curl https://api.example.com/me \
      -H 'Authorization: Bearer your_token' | jq '.roles, .permissions'
  3. 03

    step 3

    For AWS: check IAM policy and resource policy

    AWS 403 (AccessDenied) fires when the IAM policy attached to your user/role doesn't include the required Action, or when an S3 bucket policy or SCP explicitly denies access. Check both the identity-based policy and the resource-based policy.

    # Which policy is denying? Check CloudTrail for the AccessDenied event
    aws cloudtrail lookup-events \
      --lookup-attributes AttributeKey=EventName,AttributeValue=AccessDenied \
      --max-results 5
    
    # Simulate before assigning to find exactly what's needed
    aws iam simulate-principal-policy \
      --policy-source-arn arn:aws:iam::ACCOUNT:role/ROLE \
      --action-names s3:PutObject \
      --resource-arns arn:aws:s3:::bucket/key
  4. 04

    step 4

    Check for IP allowlist or rate-limit blocks

    Some APIs and web application firewalls block by IP. A 403 that appears suddenly for one client and not others often indicates an IP-level block. Check if the request works from a different network or behind a VPN.

    # Check if you're being blocked at the WAF/CDN layer
    curl -si https://api.example.com/endpoint | grep -i 'x-cache\|x-amzn\|cf-ray\|x-waf'
    # cf-ray header = Cloudflare WAF blocked you
    # x-amzn-errortype = AWS WAF
  5. 05

    step 5

    For CORS: verify the Origin is allowed

    A CORS 403 happens when the server's allowed-origins list doesn't include your frontend's origin. The response will have no `Access-Control-Allow-Origin` header. The fix is on the server — add your origin to the allowed list.

    # Test CORS preflight manually
    curl -si https://api.example.com/endpoint \
      -H 'Origin: https://yourapp.com' \
      -H 'Access-Control-Request-Method: POST' \
      -X OPTIONS
    # Check response for Access-Control-Allow-Origin

How to verify the fix

  • The 403 no longer appears after correcting permissions.
  • Least-privilege check: your role only has the permissions it needs, not more.
  • For AWS: run `simulate-principal-policy` to confirm the required actions are now allowed.

Why 403 happens at the runtime level

HTTP 403 indicates the server has successfully authenticated the requester (or determined authentication is irrelevant) but authorization checks failed. The authorization decision tree evaluates: identity policies (IAM roles, JWT scopes, RBAC roles), resource policies (S3 bucket ACLs, database row-level security), environmental checks (IP allowlist, time-of-day restrictions, geo-blocking), and explicit deny rules which override any allow. An explicit DENY in AWS IAM always wins regardless of other allows — this is a common source of confusing 403s.

Common debug mistakes for 403

  • Confusing 403 with 401 — adding credentials won't fix a permission error, but many developers retry with auth headers first.
  • In AWS, an explicit `Deny` statement in an SCP or resource policy overriding an `Allow` — the `Effect: Deny` always wins.
  • File system permissions on a self-hosted API where the web server process doesn't have read access to the file being served.
  • CORS misconfiguration where the allowed-origins list uses trailing slashes (`https://app.com/`) vs no trailing slash (`https://app.com`).
  • Using a production API key in development against a staging server that has IP restrictions applied.

When 403 signals a deeper problem

Persistent 403s in a microservices environment often signal that authorization is implemented inconsistently across services — each team defining its own permission model, resulting in users having access in service A but not service B for the same conceptual operation. The architectural fix is centralizing authorization into a dedicated service (Open Policy Agent, Cedar, or Casbin) with a unified policy language and a single source of truth for roles. This also makes permission auditing possible, which is a compliance requirement in SOC 2 and ISO 27001 environments.

Editor's take

The 403 is the most security-sensitive status code to get wrong. The classic production mistake: a developer changes a query from `WHERE user_id = $current_user_id` to `WHERE id = $request_param` to support admin lookups, forgets to add an authorization check, and ships an IDOR (insecure direct object reference) vulnerability. The server returns 200 for any user's data to anyone who knows the ID. The correct behavior was a 403 that never came.

On the infrastructure side, 403 from AWS is a debugging rabbit hole that seniors navigate by going straight to CloudTrail rather than guessing. The `errorCode` in the CloudTrail event tells you the exact denied action. The `userIdentity` field tells you which assumed role made the call — critical in ECS/Lambda environments where the task role is different from the developer's IAM user. Comparing the denied action against the task role's attached policies (not the inline policies, which are separate) takes experience to do correctly and fast.

The adjacent error seen most often alongside 403 clusters: 404 that should be 403 — some APIs deliberately return 404 on resources the user can't access to avoid disclosing whether the resource exists at all. This is correct security practice but infuriating to debug. Stripe does this. AWS S3 does this. If you're hitting what looks like a 404 but suspect it might be a permissions issue, check whether adding proper credentials changes the response code.

By Bikram Nath · Curator · Updated June 2026

Frequently asked questions

What is the difference between 401 and 403?

401 means 'prove who you are — send credentials.' 403 means 'I know who you are, and the answer is no.' A 401 implies re-authenticating might help. A 403 means your account genuinely lacks permission — re-authenticating won't change the outcome; you need a role or permission change.

Why does AWS return 403 instead of a more specific error?

AWS returns 403 (AccessDenied) deliberately — revealing a more specific error would help attackers enumerate what resources exist. If you see 403 on S3, it might mean the bucket doesn't exist (if you don't have permission to list), the key doesn't exist, or you genuinely lack access. CloudTrail is your source of truth for exactly which action was denied and why.

Can a 403 happen without any authentication attempt?

Yes. File servers, CDNs, and object storage often return 403 when a path is blocked by directory listing being disabled, a `.htaccess` rule, or a bucket ACL that denies public access. In these cases, authentication isn't the issue — the resource is simply inaccessible to any requester.

disclosure:Errordex runs AdSense, has zero third-party affiliate or sponsored links, and occasionally links to the editor’s own paid digital products (clearly labelled). Every fix is cross-referenced against the official sources listed in the “sources” sidebar before it ships. If a fix here didn’t work for you, please email so we can update the page.