Returning HTTP 200 with an error body on authorization failures breaks API client error handling — clients that check response.ok will treat the failure as a success. Returning HTTP 500 from an unhandled exception when unauthorized access is attempted leaks implementation details (CWE-209). Returning HTTP 302 redirects from API routes sends HTML to JSON clients. These response code errors make authorization failures invisible to automated monitoring and difficult to debug. OWASP A01 (Broken Access Control) and CWE-284 both require that unauthorized access be denied with correct HTTP semantics, not masked behind incorrect status codes.
Medium because incorrect status codes obscure authorization failures from clients and monitoring, increasing mean time to detection when access controls are bypassed.
Return explicit, semantically correct status codes: 401 for unauthenticated, 403 for authenticated-but-unauthorized, and 404 when the resource's existence should not be revealed to the caller.
export async function GET(req: NextRequest, { params }) {
const session = await auth();
if (!session) {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
}
const resource = await db.document.findFirst({
where: { id: params.id, userId: session.user.id },
});
if (!resource) {
// 404 hides whether the document exists at all
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(resource);
}
Never return 200 with an error payload from authorization failures — this pattern silently breaks every client that relies on HTTP status codes for error detection.
ID: saas-authorization.api-auth.authorization-fails-403
Severity: medium
What to look for: Count all relevant instances and enumerate each. Examine what happens in route handlers when an authorization check fails. Look at the response returned when session is null, when a user tries to access another user's resource, or when a role check fails. Are responses returning status: 401 or status: 403? Or are they returning status: 200 with an error message in the body, or status: 500 from an unhandled exception?
Pass criteria: Authentication failures (no session) return HTTP 401. At least 1 implementation must be verified. Authorization failures (authenticated but insufficient permission) return HTTP 403 or HTTP 404 (for resources that should not reveal their existence to unauthorized callers). These are returned as explicit responses, not as unhandled exceptions.
Fail criteria: Authorization failures return HTTP 200 with an error body (bad for API clients), HTTP 500 from an unhandled throw or database error when unauthorized access is attempted, or redirect to the login page without proper HTTP status codes in API routes.
Skip (N/A) when: Unable to determine the error handling patterns from static analysis of the codebase.
Detail on fail: "Authorization failures return [incorrect status code] instead of 401/403/404. This breaks API client error handling and may leak information." (Note the pattern found.)
Remediation: Return explicit HTTP status codes from authorization failures. Use 401 for "not authenticated," 403 for "authenticated but not allowed," and 404 for "not allowed to know this exists."
export async function GET(req: NextRequest, { params }) {
const session = await auth();
if (!session) {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
}
const resource = await db.document.findFirst({
where: { id: params.id, userId: session.user.id },
});
if (!resource) {
// Use 404 to avoid revealing whether the document exists
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(resource);
}