Introduction

Welcome to the lesson on exposing sensitive user data, part of the broken access control course! In this lesson, we will explore how sensitive user data can be inadvertently exposed in web applications, leading to potential security breaches.

Understanding what constitutes sensitive data and how to protect it is crucial for maintaining user trust and complying with regulations. Let's dive in and learn how to safeguard sensitive information! 🔍

Understanding Sensitive User Data

Sensitive user data includes any information that, if exposed, could harm an individual or organization. Protecting this data is essential to prevent unauthorized access and maintain user trust. Key examples of sensitive data include:

  • Passwords: Critical for user authentication and must be protected to prevent unauthorized access.
  • Personal identification numbers (PINs): Used for identity verification and require strict confidentiality.
  • Financial information: Includes credit card numbers and bank account details, which are highly sensitive and targeted by attackers.

In this section, we will discuss why safeguarding sensitive data is a fundamental aspect of web application security, setting the stage for examining vulnerable code.

The Vulnerable Code

Let's examine a code snippet that demonstrates how sensitive user data can be exposed due to improper data handling:

@RestController
@RequestMapping("/api/user")
public class UserController {
    private final UserRepository users;
    private final JwtUtil jwt;

    public UserController(UserRepository users, JwtUtil jwt) {
        this.users = users;
        this.jwt = jwt;
    }

    @GetMapping("/details")
    public ResponseEntity<?> getUserDetails(@RequestHeader(value = "authorization", required = false) String authorization) {
        var outcome = getCurrentUser(authorization);
        if (outcome.error() != null) {
            return outcome.error();
        }
        
        User user = outcome.user();
        
        // Vulnerable: Returns password hash along with user data
        return ResponseEntity.ok(new UserDetailsResponse(
            user.getId(),
            user.getUsername(),
            user.getPassword(),  // Exposing hashed password - BAD!
            user.getRole().toString()
        ));
    }
    
    private AuthOutcome getCurrentUser(String authorizationHeader) {
        if (authorizationHeader == null || authorizationHeader.isBlank()) {
            return new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "Authentication required"));
        }
        String token = authorizationHeader.startsWith("Bearer ") ? authorizationHeader.substring(7) : authorizationHeader;
        try {
            DecodedJWT decoded = jwt.verify(token);
            Integer userId = decoded.getClaim("userId").asInt();
            return users.findById(userId)
                    .map(user -> new AuthOutcome(user, null))
                    .orElseGet(() -> new AuthOutcome(null, error(HttpStatus.NOT_FOUND, "User not found")));
        } catch (Exception e) {
            return new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "Invalid token"));
        }
    }
    
    private ResponseEntity<ErrorResponse> error(HttpStatus status, String detail) {
        return ResponseEntity.status(status).body(new ErrorResponse(detail));
    }
    
    private record AuthOutcome(User user, ResponseEntity<ErrorResponse> error) {}
    
    public record UserDetailsResponse(Integer id, String username, String password, String role) {}
}

In this code, the API endpoint returns the entire user information including the hashed password. Exposing passwords, even in hashed form, is a critical security vulnerability that can lead to account compromise and potential system-wide breaches. Understanding how this vulnerability can be exploited is crucial for recognizing the importance of secure coding practices.

Exploiting the Vulnerability
Step 1: Ensure Password Hashing During Registration

Now that we understand the importance of password hashing, let's implement it during user registration using Spring Security's PasswordEncoder with BCrypt:

@RestController
@RequestMapping("/api/auth")
public class AuthController {
    private final UserRepository users;
    private final JwtUtil jwt;
    private final PasswordEncoder passwordEncoder;

    public AuthController(UserRepository users, JwtUtil jwt, PasswordEncoder passwordEncoder) {
        this.users = users;
        this.jwt = jwt;
        this.passwordEncoder = passwordEncoder;
    }

    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
        String username = request.username();
        String password = request.password();
        String roleRaw = request.role() == null ? "user" : request.role();

        if (username == null || password == null) {
            return error(HttpStatus.BAD_REQUEST, "Username and password are required");
        }

        if (users.findByUsername(username).isPresent()) {
            return error(HttpStatus.BAD_REQUEST, "Username already exists");
        }

        // Hash the password before saving in the database
        Role role = switch (roleRaw) {
            case "admin" -> Role.ADMIN;
            default -> Role.USER;
        };
        User u = new User();
        u.setUsername(username);
        u.setPassword(passwordEncoder.encode(password));  // BCrypt hashing
        u.setRole(role);
        users.save(u);

        return ResponseEntity.ok(new RegisterResponse("User registered successfully", u.getId()));
    }
    
    private ResponseEntity<ErrorResponse> error(HttpStatus status, String detail) {
        return ResponseEntity.status(status).body(new ErrorResponse(detail));
    }
    
    public record RegisterRequest(String username, String password, String role) {}
    public record RegisterResponse(String message, Integer userId) {}
}

Here, we use Spring Security's PasswordEncoder, configured with BCrypt (a strong cryptographic hashing algorithm), to secure passwords. The encode method takes the plain password and creates a unique hash that is computationally expensive to crack, even if the database is compromised. BCrypt automatically handles salt generation, making each hash unique even for identical passwords.

With password hashing in place, we can now focus on ensuring that sensitive information is not exposed in API responses.

Step 2: Create a DTO for Non-Sensitive Data

Even with hashed passwords, we should never expose them in API responses. We will use DTOs (data transfer objects) to control exactly which fields are returned. A DTO is a simple object that carries data between processes, allowing us to define precisely what information should be exposed.

Let's create a DTO class that includes only non-sensitive fields:

public class UserDTO {
    private Integer id;
    private String username;
    private String role;
    
    // Constructor that maps from User entity
    public UserDTO(User user) {
        this.id = user.getId();
        this.username = user.getUsername();
        this.role = user.getRole().toString();
        // Note: password is intentionally excluded
    }
    
    // Getters
    public Integer getId() {
        return id;
    }
    
    public String getUsername() {
        return username;
    }
    
    public String getRole() {
        return role;
    }
}

In this UserDTO class, we include only the id, username, and role fields. The password field is intentionally excluded, ensuring that it can never be accidentally exposed in an API response. The constructor accepts a User entity and maps the non-sensitive fields, providing a clean separation between internal data and external representation.

Step 3: Integrate the DTO in the Controller

Now that we have our DTO, let's update the controller to use it when returning user information:

@RestController
@RequestMapping("/api/user")
public class UserController {
    private final UserRepository users;
    private final JwtUtil jwt;

    public UserController(UserRepository users, JwtUtil jwt) {
        this.users = users;
        this.jwt = jwt;
    }

    @GetMapping("/details")
    public ResponseEntity<?> getUserDetails(@RequestHeader(value = "authorization", required = false) String authorization) {
        var outcome = getCurrentUser(authorization);
        if (outcome.error() != null) {
            return outcome.error();
        }
        
        User user = outcome.user();
        
        // Secure: Returns only non-sensitive data using DTO
        UserDTO userDTO = new UserDTO(user);
        return ResponseEntity.ok(userDTO);
    }
    
    private AuthOutcome getCurrentUser(String authorizationHeader) {
        if (authorizationHeader == null || authorizationHeader.isBlank()) {
            return new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "Authentication required"));
        }
        String token = authorizationHeader.startsWith("Bearer ") ? authorizationHeader.substring(7) : authorizationHeader;
        try {
            DecodedJWT decoded = jwt.verify(token);
            Integer userId = decoded.getClaim("userId").asInt();
            return users.findById(userId)
                    .map(user -> new AuthOutcome(user, null))
                    .orElseGet(() -> new AuthOutcome(null, error(HttpStatus.NOT_FOUND, "User not found")));
        } catch (Exception e) {
            return new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "Invalid token"));
        }
    }
    
    private ResponseEntity<ErrorResponse> error(HttpStatus status, String detail) {
        return ResponseEntity.status(status).body(new ErrorResponse(detail));
    }
    
    private record AuthOutcome(User user, ResponseEntity<ErrorResponse> error) {}
}

In this updated code, we create a UserDTO instance from the User entity before returning it in the response. By mapping the User entity to a UserDTO, we ensure that sensitive information like the password hash is never exposed in API responses. This approach provides a clear boundary between our internal data model and the data we expose to clients, significantly reducing the risk of data exposure.

Real-World Examples and Impact

Exposing even hashed passwords can be dangerous. While properly hashed passwords are computationally expensive to crack, they are not impossible to break with modern hardware and techniques like rainbow tables. Attackers can perform offline cracking attempts on the hashed passwords, potentially compromising user accounts. Key points to consider include:

  • Data breaches: Major companies have suffered data breaches due to exposed sensitive information, leading to financial losses and reputational damage.
  • Legal consequences: Failing to protect sensitive data can result in legal actions and fines, especially under regulations like GDPR.
  • User trust: Users expect their data to be secure. Breaches can erode trust and lead to user attrition.

These examples underscore the importance of never exposing password hashes in API responses and only returning non-sensitive fields necessary for the application's functionality. With these insights, we can conclude our lesson and prepare for the next steps in enhancing web application security.

Conclusion and Next Steps

In this lesson, we explored the critical issue of exposing sensitive user data and how to mitigate the associated risks. By understanding common vulnerabilities and implementing secure coding practices, you can protect sensitive information and enhance the security of your web applications.

Key takeaways include:

  • Always hash passwords using Spring Security's BCryptPasswordEncoder before storing them.
  • Never expose password hashes in API responses.
  • Use DTOs to control which fields are returned from your endpoints.
  • Only return data that is necessary for the application's functionality.

As you move on to the practice exercises, apply what you have learned to reinforce your understanding. In the next lesson, we will continue our exploration of web application security, focusing on cryptographic failures. Keep up the great work! 🚀

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