Sensitive data overexposure occurs when an API returns more data, objects, or identifiers than a requester needs or is authorized to receive. The vulnerability often appears when an endpoint exposes a complete internal record instead of a deliberately limited response. section 4.6.2 section 4.11
Attackers can use exposed personal information to build PII breaches, scrape user profiles, infer internal data models, or learn how business rules such as insurance premiums work. Unauthenticated access and weak rate limiting can multiply the damage. section 4.6.2 section 6.0
Common forms include:
- Public endpoints returning identifying details such as email addresses, dates of birth, or user IDs. section 4.6.2
- Responses exposing database or internal data models, including fields that reveal business rules and implementation details. section 4.6.2
- Full objects, such as special deals, returned to customers who should not see them. section 4.6.2
- Unnecessary identifiers, such as a driver or vehicle ID disclosed by a ride-sharing application. section 4.6.2
User lookup by email
A GET /query-user?email=susan@example.com endpoint returns a matching user profile. The vulnerable implementation checks only whether the email exists, so anyone who discovers addresses can retrieve profiles. A safer implementation first identifies the requesting user, then requires the requested user to share the same project; otherwise it returns 404. This prevents random profile scraping. section 4.6.3
Prevent the flaw by designing response models separately from internal data models, selecting only necessary fields, checking permissions before returning data, requiring authentication for sensitive flows, and applying rate limits and abnormal-behavior detection. Test endpoints for unauthorized profiles, hidden fields, excess objects, and unnecessary identifiers. section 4.6.2 section 6.0 section 4.11
Listing 4.9 Querying users without excessive data exposure
# file: ch04/excessive_data_exposure.py
@server.get("/query-user", response_model=UserProfileSchema)
async def query_user(
email: str, user_claims: UserClaims = Depends(validate_access)
):
with session_maker() as session:
requesting_user = session.scalar( #1
UserModel.id == user_claims.sub,
)
requested_user = session.scalar(select(UserModel).where(
UserModel.email == email,
UserModel.project == requesting_user.project, #2
))
if requested_user is None:
raise HTTPException(
status_code=404,
detail=f"User with email {email} not found."
)
return {
"id": requested_user.id,
"email": requested_user.email,
"project": requested_user.project,
"full_name": requested_user.full_name,
}
Compare the vulnerable lookup query with the corrected query, which adds the requesting user’s project to the authorization condition. section 4.6.3
