From c33c62298c119e99218b9471c3859461aa9712fc Mon Sep 17 00:00:00 2001 From: Toby Moreno Date: Sun, 9 Aug 2026 09:36:41 -0700 Subject: [PATCH] fix: enforce authoritative roles in IDOR level 4 --- .../vulnerability/idor/IDORVulnerability.java | 22 +++--- .../idor/IDORVulnerabilityTest.java | 76 +++++++++++++++---- 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerability.java index d23f59e2e..4ee81f3b7 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerability.java @@ -1,6 +1,5 @@ package org.sasanlabs.service.vulnerability.idor; -import java.util.Base64; import java.util.List; import org.sasanlabs.internal.utility.LevelConstants; import org.sasanlabs.internal.utility.Variant; @@ -204,12 +203,16 @@ public ResponseEntity> level4( if (actualToken != null) { User decodedUser = idorLoginService.decodeToken(actualToken); int tokenUserId = decodedUser.getUserId(); - String role = cookieRole != null ? decodeBase64(cookieRole) : decodedUser.getRole(); if (id == null) { id = tokenUserId; } + String role = fetchRoleById(tokenUserId); + if (role == null) { + return response(INVALID_USER, false); + } + if (ROLE_ADMIN.equalsIgnoreCase(role) || tokenUserId == id) { User profile = fetchUserById(id); if (profile == null) { @@ -293,6 +296,13 @@ private User fetchUserById(int id) { return users.get(0); } + private String fetchRoleById(int id) { + List roles = + jdbcTemplate.query( + SQL_ROLE_BY_ID, new Object[] {id}, (rs, rowNum) -> rs.getString("role")); + return roles.isEmpty() ? null : roles.get(0); + } + private List fetchAllUsers() { return jdbcTemplate.query( SQL_ALL_PROFILES, @@ -304,14 +314,6 @@ private List fetchAllUsers() { rs.getString("role"))); } - private String decodeBase64(String encodedId) { - try { - return new String(Base64.getUrlDecoder().decode(encodedId)); - } catch (IllegalArgumentException e) { - return null; - } - } - private ResponseEntity> response( Object content, boolean isValid) { return new ResponseEntity<>( diff --git a/src/test/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerabilityTest.java b/src/test/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerabilityTest.java index 12b3b1cbd..e2dede90e 100644 --- a/src/test/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerabilityTest.java +++ b/src/test/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerabilityTest.java @@ -100,7 +100,7 @@ void level3_ShouldAllowRoleEscalationWhenRoleCookieIsAdmin() { } @Test - void level4_ShouldAllowOpaqueIdAccess() { + void level4_ShouldRejectRoleEscalationThroughEncodedCookie() { String encodedRole = java.util.Base64.getUrlEncoder() .withoutPadding() @@ -112,19 +112,18 @@ void level4_ShouldAllowOpaqueIdAccess() { User decoded = new User(); decoded.setUserId(1); decoded.setRole("USER"); - User bob = new User(2, "Bob", 60000, "USER"); - when(idorLoginService.decodeToken(escalatedToken)).thenReturn(decoded); when(jdbcTemplate.query( - anyString(), + eq(SQL_ROLE_BY_ID), any(Object[].class), any(org.springframework.jdbc.core.RowMapper.class))) - .thenReturn(Arrays.asList(bob)); + .thenReturn(Arrays.asList("USER")); ResponseEntity> response = idor.level4(escalatedToken, encodedRole, 2); - assertTrue(response.getBody().getIsValid()); + assertFalse(response.getBody().getIsValid()); + assertEquals("Access Denied - Insufficient privileges", response.getBody().getContent()); } @Test @@ -173,15 +172,10 @@ void level3_ShouldRejectInvalidToken() { } @Test - void level4_ShouldAllowAccessToAnyOpaqueIdRegardlessOfRole() { + void level4_ShouldAllowDatabaseConfirmedAdminAccessToAnyId() { String userToken = java.util.Base64.getEncoder() .encodeToString("{\"userId\":1,\"role\":\"USER\"}".getBytes()); - String encodedRole = - java.util.Base64.getUrlEncoder() - .withoutPadding() - .encodeToString("ADMIN".getBytes()); - User decoded = new User(); decoded.setUserId(1); decoded.setRole("USER"); @@ -189,15 +183,69 @@ void level4_ShouldAllowAccessToAnyOpaqueIdRegardlessOfRole() { when(idorLoginService.decodeToken(userToken)).thenReturn(decoded); when(jdbcTemplate.query( - anyString(), + eq(SQL_ROLE_BY_ID), + any(Object[].class), + any(org.springframework.jdbc.core.RowMapper.class))) + .thenReturn(Arrays.asList("ADMIN")); + when(jdbcTemplate.query( + eq(SQL_PROFILE_BY_ID), any(Object[].class), any(org.springframework.jdbc.core.RowMapper.class))) .thenReturn(Arrays.asList(bob)); ResponseEntity> response = - idor.level4(userToken, encodedRole, 2); + idor.level4(userToken, null, 2); + + assertTrue(response.getBody().getIsValid()); + assertEquals("ADMIN", ((User) response.getBody().getContent()).getRole()); + } + + @Test + void level4_ShouldAllowUserToAccessOwnProfile() { + String userToken = "valid-user-token"; + User decoded = new User(); + decoded.setUserId(1); + decoded.setRole("ADMIN"); + User profile = new User(1, "Alice", 50000, "USER"); + + when(idorLoginService.decodeToken(userToken)).thenReturn(decoded); + when(jdbcTemplate.query( + eq(SQL_ROLE_BY_ID), + any(Object[].class), + any(org.springframework.jdbc.core.RowMapper.class))) + .thenReturn(Arrays.asList("USER")); + when(jdbcTemplate.query( + eq(SQL_PROFILE_BY_ID), + any(Object[].class), + any(org.springframework.jdbc.core.RowMapper.class))) + .thenReturn(Arrays.asList(profile)); + + ResponseEntity> response = + idor.level4(userToken, null, 1); assertTrue(response.getBody().getIsValid()); + assertEquals("USER", ((User) response.getBody().getContent()).getRole()); + } + + @Test + void level4_ShouldRejectTokenIdentityMissingFromDatabase() { + String userToken = "deleted-user-token"; + User decoded = new User(); + decoded.setUserId(99); + decoded.setRole("ADMIN"); + + when(idorLoginService.decodeToken(userToken)).thenReturn(decoded); + when(jdbcTemplate.query( + eq(SQL_ROLE_BY_ID), + any(Object[].class), + any(org.springframework.jdbc.core.RowMapper.class))) + .thenReturn(Arrays.asList()); + + ResponseEntity> response = + idor.level4(userToken, null, 1); + + assertFalse(response.getBody().getIsValid()); + assertEquals("Invalid user", response.getBody().getContent()); } @Test