Introduction

Welcome to the third lesson of our broken access control course! In this lesson, we will explore the concept of forced browsing, a technique used by attackers to exploit predictable URL patterns and gain unauthorized access to sensitive areas of a web application.

Understanding forced browsing is crucial for implementing effective access control mechanisms and protecting your applications from unauthorized access. Let's dive in and learn how to identify and mitigate these vulnerabilities. 🚀

Understanding Forced Browsing

Forced browsing occurs when attackers manipulate predictable URL patterns to access unauthorized pages within a web application. This vulnerability often arises when developers do not implement proper access control mechanisms, allowing attackers to bypass authentication and authorization checks. For example, if a web application has predictable URLs like /admin/dashboard or /user/settings, an attacker might try to access these URLs directly to gain unauthorized access.

Additionally, attackers often use automated tools like DirBuster, Gobuster, or Burp Suite's content discovery feature to systematically scan and discover unprotected endpoints. These tools work by testing thousands of common endpoint patterns against a target application, making it easier for attackers to find vulnerable administrative interfaces or sensitive resources that lack proper access controls.

With that being said, let's look at a code example to understand what this vulnerability might look like.

Vulnerable Code Example

Let's examine a code snippet that demonstrates a vulnerable admin endpoint susceptible to forced browsing. This example highlights how a lack of access control can lead to unauthorized access to sensitive information.

@RestController
@RequestMapping("/api/admin")
public class AdminController {
    private final UserRepository users;
    private final JwtUtil jwt;
    
    public AdminController(UserRepository users, JwtUtil jwt) {
        this.users = users;
        this.jwt = jwt;
    }

    // Admin endpoint that should only be accessible by administrators
    @GetMapping("/getAppInfo")
    public ResponseEntity<?> getAppInfo() {
        // This endpoint returns sensitive configuration and metrics
        // that should only be available to admin users
        return ResponseEntity.ok("Sensitive admin app info");
    }
}

In this code, the /api/admin/getAppInfo endpoint is exposed without any access control checks. This means anyone who knows the URL can access sensitive admin information, making it vulnerable to forced browsing attacks.

Next, we'll see how an attacker can exploit this vulnerability.

Exploiting the Vulnerability

An attacker can easily exploit this vulnerability by sending a request to the vulnerable endpoint. Here is how this can be done using a simple command-line tool:

curl "http://localhost:3000/api/admin/getAppInfo"

This command uses curl to send a request to the /getAppInfo endpoint. Since there are no access controls in place, the server responds with the sensitive admin information, demonstrating how easily an attacker can exploit this vulnerability.

To prevent such exploitation, we need to implement access control mechanisms.

Implementing Role-Based Access Control

To secure the endpoint, we need to implement role-based access control (RBAC). This ensures that only users with the appropriate role can access sensitive information. Remember, it's critical to enforce these checks on the server-side - hiding admin panels or disabling UI elements with JavaScript won't prevent attackers from making direct API requests.

Let's create a method that checks if the authenticated user has admin privileges:

private AdminOutcome verifyAdminOrError(String authorizationHeader) {
    if (authorizationHeader == null || authorizationHeader.isBlank()) {
        return new AdminOutcome(null, error(HttpStatus.UNAUTHORIZED, "Missing authorization header"));
    }
    
    String token = authorizationHeader.startsWith("Bearer ") 
        ? authorizationHeader.substring(7) 
        : authorizationHeader;
    
    try {
        // Verify the JWT token
        DecodedJWT decoded = jwt.verify(token);
        Integer userId = decoded.getClaim("userId").asInt();
        
        // Look up the user in the database using the trusted userId
        var userOpt = users.findById(userId);
        
        if (userOpt.isEmpty()) {
            return new AdminOutcome(null, error(HttpStatus.UNAUTHORIZED, "User not found"));
        }
        
        User user = userOpt.get();
        
        // Check if the user has admin role
        if (user.getRole() != Role.ADMIN) {
            return new AdminOutcome(null, error(HttpStatus.FORBIDDEN, "Access denied"));
        }
        
        return new AdminOutcome(user, null);
    } catch (Exception e) {
        return new AdminOutcome(null, error(HttpStatus.UNAUTHORIZED, "Invalid token"));
    }
}

private record AdminOutcome(User user, ResponseEntity<ErrorResponse> error) {}

This method checks the user's role and returns an AdminOutcome containing either the verified admin user or an error response. The security of this implementation relies on proper authentication:

  1. When users log in, they receive a JWT token containing their userId.
  2. Before accessing the endpoint, the JWT token is verified and decoded.
  3. The decoded userId is used to look up the user from the database.
  4. We then check if the user has the ADMIN role.

This ensures that users cannot manipulate their role or impersonate other users, as the userId comes from a verified JWT token, not from user input.

Securing the Endpoint

Now, let's apply the RBAC to the endpoint to ensure it is secure:

@RestController
@RequestMapping("/api/admin")
public class AdminController {
    private final UserRepository users;
    private final JwtUtil jwt;
    
    public AdminController(UserRepository users, JwtUtil jwt) {
        this.users = users;
        this.jwt = jwt;
    }

    @GetMapping("/getAppInfo")
    public ResponseEntity<?> getAppInfo(
            @RequestHeader(value = "authorization", required = false) String authorization) {
        // Verify admin access before proceeding
        var outcome = verifyAdminOrError(authorization);
        if (outcome.error() != null) {
            return outcome.error();
        }
        
        // Only executed if user is an admin
        return ResponseEntity.ok("Sensitive admin app info (secured)");
    }
    
    private AdminOutcome verifyAdminOrError(String authorizationHeader) {
        // ... implementation shown above ...
    }
    
    private ResponseEntity<ErrorResponse> error(HttpStatus status, String detail) {
        return ResponseEntity.status(status).body(new ErrorResponse(detail));
    }
    
    private record AdminOutcome(User user, ResponseEntity<ErrorResponse> error) {}
}

The endpoint now calls verifyAdminOrError() at the beginning and immediately returns any error response if verification fails. This ensures that only authenticated users with the ADMIN role can access the endpoint. If the user doesn't have the required role or provides an invalid token, the method returns an appropriate error response (401 Unauthorized or 403 Forbidden).

By implementing RBAC, we ensure that only authorized admin users can access the endpoint, significantly reducing the risk of forced browsing attacks.

With these measures in place, the endpoint is now secure against unauthorized access.

Conclusion and Next Steps

In this lesson, we explored the concept of forced browsing and learned how to secure endpoints using role-based access control. By understanding and mitigating these vulnerabilities, you can protect your web applications from unauthorized access.

Now, it's time to apply what you've learned in the practice exercises that follow. Keep up the great work, and continue exploring web application security in the upcoming lessons! 🚀

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal