Direct Request, also called forced browsing, occurs when a web application fails to properly check whether a user has permission to access a specific URL…
Direct Request, also called forced browsing, occurs when a web application fails to properly check whether a user has permission to access a specific URL, file, or resource before serving it. An attacker can bypass the intended navigation flow by guessing or discovering the direct path to restricted content—such as admin panels, user data, or configuration files—and accessing it without authorization.
02How It Happens
Web applications often rely on navigation menus, buttons, or session state to control what users see, but they may not enforce authorization checks at the server level for every accessible URL. When a developer assumes that hiding a link in the UI is sufficient protection, or forgets to add permission validation to certain endpoints, an attacker can simply type or request the URL directly in their browser. This is especially common in applications that use predictable URL patterns (e.g., /admin, /user/123, /backup.sql) or fail to validate user roles before returning sensitive data.
The weakness stems from a gap between *authentication* (confirming who you are) and *authorization* (confirming what you're allowed to do). A user may be logged in, but the application doesn't verify they have the right role or permission for that specific resource.
03Real-World Impact
An attacker exploiting this weakness can access administrative dashboards, view other users' private information, download backup files, modify settings, or retrieve sensitive configuration data. In some cases, direct access to unprotected endpoints can lead to account takeover, data breaches, or complete site compromise. The impact depends on what resources are left unprotected—a forgotten admin panel is far more serious than an unlisted help page.
04Vulnerable & Fixed Patterns
Vulnerable pattern
from flask import Flask, request, session
app = Flask(__name__)
@app.route('/admin/users')
def admin_users():
# No authorization check — assumes only admins will know this URL
users = database.query("SELECT * FROM users")
return render_template('admin_users.html', users=users)
@app.route('/user/<user_id>/profile')
def user_profile(user_id):
# No check that logged-in user owns this profile
profile = database.query(f"SELECT * FROM users WHERE id = {user_id}")
return render_template('profile.html', profile=profile)
Why it's vulnerable: The endpoints perform no authorization checks. Any authenticated (or even unauthenticated) user can request /admin/users or /user/999/profile directly and receive the data, regardless of their role or ownership.
Fixed pattern
from flask import Flask, request, session, abort
app = Flask(__name__)
def require_admin(f):
def wrapper(*args, **kwargs):
if session.get('role') != 'admin':
abort(403)
return f(*args, **kwargs)
return wrapper
@app.route('/admin/users')
@require_admin
def admin_users():
users = database.query("SELECT * FROM users")
return render_template('admin_users.html', users=users)
@app.route('/user/<user_id>/profile')
def user_profile(user_id):
if session.get('user_id') != int(user_id):
abort(403)
profile = database.query("SELECT * FROM users WHERE id = ?", (user_id,))
return render_template('profile.html', profile=profile)
Vulnerable pattern
<?php
// No authorization check
if ($_GET['action'] === 'delete_user') {
$user_id = $_GET['user_id'];
$wpdb->query("DELETE FROM users WHERE ID = $user_id");
echo "User deleted";
}
// Admin panel — assumes only admins know the URL
if ($_GET['page'] === 'admin_settings') {
$settings = get_option('site_settings');
echo json_encode($settings);
}
?>
Why it's vulnerable: Neither endpoint checks the user's role or permissions. Any visitor who knows the URL can delete users or view admin settings.
Fixed pattern
<?php
function require_admin() {
if (!current_user_can('manage_options')) {
wp_die('Unauthorized', 403);
}
}
if ($_GET['action'] === 'delete_user') {
require_admin();
$user_id = intval($_GET['user_id']);
$wpdb->query($wpdb->prepare("DELETE FROM users WHERE ID = %d", $user_id));
echo "User deleted";
}
if ($_GET['page'] === 'admin_settings') {
require_admin();
$settings = get_option('site_settings');
echo json_encode($settings);
}
?>
05Prevention Checklist
Check authorization on every endpoint. Before returning or modifying any resource, verify the user's role and permissions server-side. Do not rely on hidden URLs or client-side checks.
Use a consistent authorization framework. Implement a centralized function or middleware (decorators, hooks, or filters) that enforces permission rules across all routes.
Validate ownership. When a user requests a resource by ID (e.g., /user/123), confirm they own or have explicit permission to access that specific record.
Deny by default. If a user's role is not explicitly granted access, deny the request. Do not assume unauthenticated users should be blocked but authenticated users should be allowed.
Test with different user roles. Manually attempt to access restricted URLs while logged in as a regular user, guest, or different role to confirm authorization is enforced.
Log and monitor access attempts. Track failed authorization attempts to detect potential forced browsing attacks.
06Signs You May Already Be Affected
Check your web server and application logs for repeated requests to URLs that should be restricted (e.g., /admin, /backup, /config). If you find successful responses (HTTP 200) to these endpoints from users who should not have access, or if you discover unlinked but functional admin pages, your application may be vulnerable. Additionally, review your codebase for routes or endpoints that lack explicit authorization checks before returning sensitive data.