Skip to main content

Overview

Forced Browsing is a vulnerability where an attacker gains access to a resource (a page, file, or endpoint) simply by knowing or guessing the URL. These resources are “protected” only because no legitimate link points to them, but they lack any access control checks. This is a specific type of Improper Access Control (CWE-284).

Business Impact

Forced Browsing can lead to the exposure of sensitive administrative panels, internal debug information, configuration files, or un-published content. This can result in data breaches or give an attacker a foothold for further attacks.

Reference Details

CWE ID: CWE-425 OWASP Top 10 (2021): A01:2021 - Broken Access Control Severity: Medium

Framework-Specific Analysis and Remediation

This vulnerability is caused by a developer creating a new endpoint and forgetting to apply the framework’s authorization controls (middleware, decorators, attributes, or filters). The solution is to ensure every single endpoint has a default “deny” policy, and only explicitly public endpoints are reachable.
  • Python
  • Java
  • .NET(C#)
  • PHP
  • Node.js
  • Ruby

Framework Context

A developer adds a URL pattern in urls.py for an admin-only view but forgets to add @login_required or @user_passes_test to the view.

Vulnerable Scenario 1: Unprotected Admin View

A view is created for a special “profit report” that should be admin-only, but no decorator is added.
# reporting/views.py
def admin_profit_report(request):
    # DANGEROUS: No check to see if request.user is logged in or is staff.
    return HttpResponse("This is the secret profit report.")
    
# myproject/urls.py
from reporting import views

urlpatterns = [
    # DANGEROUS: Anyone who guesses /reports/profit-panel/
    # can access this view.
    path('reports/profit-panel/', views.admin_profit_report),
]

Vulnerable Scenario 2: Unprotected Debug Endpoint

A developer adds a temporary view to debug system state but forgets to remove it or protect it.
# myapp/views.py
def debug_system_state(request):
    # DANGEROUS: This view leaks internal state
    # and has no authorization checks.
    return JsonResponse(System.get_debug_info())
    
# myproject/urls.py
urlpatterns = [
    # DANGEROUS: This endpoint is not linked from anywhere,
    # but an attacker can find it with scanners.
    path('__debug/system-state/', views.debug_system_state),
]

Mitigation and Best Practices

Wrap the view in login_required and user_passes_test(lambda u: u.is_staff) in urls.py or apply them as decorators in views.py.

Secure Code Example

# reporting/views.py (Secure)
from django.contrib.auth.decorators import login_required, user_passes_test

@login_required
@user_passes_test(lambda u: u.is_staff)
def admin_profit_report(request):
    # SECURE: Only logged-in staff can see this.
    return HttpResponse("This is the secret profit report.")
    
# myapp/views.py (Secure)
@login_required
@user_passes_test(lambda u: u.is_superuser)
def debug_system_state(request):
    # SECURE: Now only superusers can access this.
    return JsonResponse(System.get_debug_info())

Testing Strategy

Write an integration test that uses an unauthenticated client to GET the /reports/profit-panel/ URL. Assert that the response is a redirect (to login) or a 403 Forbidden, not a 200 OK.
# reporting/tests.py
def test_admin_report_is_inaccessible_by_anonymous_user(self):
    # self.client is logged out
    response = self.client.get('/reports/profit-panel/')
    # Should redirect to the login page
    self.assertRedirects(response, '/login/?next=/reports/profit-panel/')