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;
}
}
}