Excessive Data Exposure

Excessive Data Exposure

APIs often return more data than necessary, exposing sensitive information that clients filter out. This vulnerability occurs when APIs return complete object representations rather than filtered responses based on the client's needs and permissions. Mobile applications are particularly vulnerable as attackers can intercept and analyze API responses.

// Java Spring Boot example: Preventing excessive data exposure
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private AuthorizationService authService;
    
    // VULNERABLE: Returns all user data
    @GetMapping("/{id}/vulnerable")
    public User getUserVulnerable(@PathVariable Long id) {
        return userService.findById(id); // Returns entire User entity
    }
    
    // SECURE: Uses DTOs and view models
    @GetMapping("/{id}")
    public ResponseEntity<UserResponseDTO> getUser(
            @PathVariable Long id,
            @AuthenticationPrincipal UserDetails currentUser) {
        
        User user = userService.findById(id);
        if (user == null) {
            return ResponseEntity.notFound().build();
        }
        
        // Determine view level based on requester
        UserViewLevel viewLevel = authService.determineViewLevel(
            currentUser, user
        );
        
        // Create appropriate DTO based on view level
        UserResponseDTO response = createUserResponse(user, viewLevel);
        
        return ResponseEntity.ok(response);
    }
    
    private UserResponseDTO createUserResponse(User user, UserViewLevel level) {
        switch (level) {
            case PUBLIC:
                return UserResponseDTO.builder()
                    .id(user.getId())
                    .username(user.getUsername())
                    .avatarUrl(user.getAvatarUrl())
                    .build();
                    
            case FRIEND:
                return UserResponseDTO.builder()
                    .id(user.getId())
                    .username(user.getUsername())
                    .avatarUrl(user.getAvatarUrl())
                    .email(maskEmail(user.getEmail()))
                    .bio(user.getBio())
                    .joinDate(user.getCreatedAt())
                    .build();
                    
            case OWN:
                return UserResponseDTO.builder()
                    .id(user.getId())
                    .username(user.getUsername())
                    .avatarUrl(user.getAvatarUrl())
                    .email(user.getEmail())
                    .bio(user.getBio())
                    .joinDate(user.getCreatedAt())
                    .settings(user.getSettings())
                    .notificationPreferences(user.getNotificationPreferences())
                    .build();
                    
            case ADMIN:
                return UserResponseDTO.builder()
                    .id(user.getId())
                    .username(user.getUsername())
                    .avatarUrl(user.getAvatarUrl())
                    .email(user.getEmail())
                    .bio(user.getBio())
                    .joinDate(user.getCreatedAt())
                    .settings(user.getSettings())
                    .notificationPreferences(user.getNotificationPreferences())
                    .accountStatus(user.getAccountStatus())
                    .lastLoginAt(user.getLastLoginAt())
                    .loginCount(user.getLoginCount())
                    .build();
                    
            default:
                throw new IllegalStateException("Unknown view level");
        }
    }
    
    // Field-level filtering with GraphQL-style field selection
    @GetMapping
    public Page<UserResponseDTO> getUsers(
            @RequestParam(required = false) String fields,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @AuthenticationPrincipal UserDetails currentUser) {
        
        // Parse requested fields
        Set<String> requestedFields = parseFields(fields);
        
        // Get allowed fields for current user
        Set<String> allowedFields = authService.getAllowedFieldsForUser(
            currentUser, "User"
        );
        
        // Intersection of requested and allowed fields
        Set<String> finalFields = requestedFields.isEmpty() ? 
            getDefaultFields(currentUser) :
            Sets.intersection(requestedFields, allowedFields);
        
        // Fetch users with projection
        Page<User> users = userService.findAll(
            PageRequest.of(page, size)
        );
        
        // Convert to DTOs with only requested fields
        return users.map(user -> projectUser(user, finalFields));
    }
    
    // JSON Views for different serialization contexts
    public class UserViews {
        public interface Public {}
        public interface Internal extends Public {}
        public interface Admin extends Internal {}
    }
    
    @Data
    public class UserResponseDTO {
        @JsonView(UserViews.Public.class)
        private Long id;
        
        @JsonView(UserViews.Public.class)
        private String username;
        
        @JsonView(UserViews.Public.class)
        private String avatarUrl;
        
        @JsonView(UserViews.Internal.class)
        private String email;
        
        @JsonView(UserViews.Internal.class)
        private LocalDateTime joinDate;
        
        @JsonView(UserViews.Admin.class)
        private String accountStatus;
        
        @JsonView(UserViews.Admin.class)
        private Map<String, Object> metadata;
    }
    
    // Dynamic field filtering
    @Component
    public class FieldFilteringAdvice implements ResponseBodyAdvice<Object> {
        
        @Override
        public Object beforeBodyWrite(
                Object body, 
                MethodParameter returnType,
                MediaType selectedContentType, 
                Class selectedConverterType,
                ServerHttpRequest request, 
                ServerHttpResponse response) {
            
            // Check if field filtering is requested
            String fields = request.getHeaders().getFirst("X-Fields");
            if (fields != null && body != null) {
                return filterFields(body, fields);
            }
            
            return body;
        }
        
        private Object filterFields(Object body, String fields) {
            // Implementation of dynamic field filtering
            // This could use reflection or Jackson's JsonNode manipulation
            return filteredBody;
        }
    }
}