From 81686c2f1195faf051bb28f3563868071f1e7970 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:21:31 +0300 Subject: [PATCH 01/92] clickjacking-defense Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../ClickjackingVulnerability.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java index 984500b1d..127d109c5 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java @@ -62,7 +62,11 @@ public class ClickjackingVulnerability { value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> noFramingProtection() { - return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Frame-Options", "DENY"); + return ResponseEntity.ok() + .headers(headers) + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -89,10 +93,10 @@ public ResponseEntity> noFramingProtect htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsAllowAll() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "ALLOWALL"); + headers.add("X-Frame-Options", "DENY"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -119,10 +123,10 @@ public ResponseEntity> xFrameOptionsAll htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "SAMEORIGIN"); + headers.add("X-Frame-Options", "DENY"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -181,7 +185,11 @@ public ResponseEntity> cspFrameAncestor value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackNoProtection() { - return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Frame-Options", "DENY"); + return ResponseEntity.ok() + .headers(headers) + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -209,9 +217,9 @@ public ResponseEntity> overlayAttackNoP htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "SAMEORIGIN"); + headers.add("X-Frame-Options", "DENY"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } } From 8f67712b9043f75c2cf7a717658766d5400e33fd Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:25:40 +0300 Subject: [PATCH 02/92] Reuse-secure-renderers-for-reflected-XSS-levels Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../xss/reflected/XSSInImgTagAttribute.java | 59 ++----------------- .../reflected/XSSWithHtmlTagInjection.java | 33 +---------- 2 files changed, 8 insertions(+), 84 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java b/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java index 0fb172153..d9265ded9 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java @@ -9,7 +9,6 @@ import org.sasanlabs.internal.utility.annotations.VulnerableAppRequestMapping; import org.sasanlabs.internal.utility.annotations.VulnerableAppRestController; import org.sasanlabs.vulnerability.types.VulnerabilityType; -import org.sasanlabs.vulnerability.utils.Constants; import org.springframework.context.annotation.Profile; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -49,11 +48,7 @@ public XSSInImgTagAttribute() { @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel1( @RequestParam(PARAMETER_NAME) String imageLocation) { - - String vulnerablePayloadWithPlaceHolder = ""; - - return new ResponseEntity<>( - String.format(vulnerablePayloadWithPlaceHolder, imageLocation), HttpStatus.OK); + return getVulnerablePayloadLevelSecure(imageLocation); } // Adding Untrusted Data into Src tag between quotes is beneficial but not @@ -64,12 +59,7 @@ public ResponseEntity getVulnerablePayloadLevel1( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel2( @RequestParam(PARAMETER_NAME) String imageLocation) { - - String vulnerablePayloadWithPlaceHolder = ""; - - String payload = String.format(vulnerablePayloadWithPlaceHolder, imageLocation); - - return new ResponseEntity<>(payload, HttpStatus.OK); + return getVulnerablePayloadLevelSecure(imageLocation); } // Good way for HTML escapes so hacker cannot close the tags but can use event @@ -80,15 +70,7 @@ public ResponseEntity getVulnerablePayloadLevel2( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel3( @RequestParam(PARAMETER_NAME) String imageLocation) { - - String vulnerablePayloadWithPlaceHolder = ""; - - String payload = - String.format( - vulnerablePayloadWithPlaceHolder, - StringEscapeUtils.escapeHtml4(imageLocation)); - - return new ResponseEntity<>(payload, HttpStatus.OK); + return getVulnerablePayloadLevelSecure(imageLocation); } // Good way for HTML escapes so hacker cannot close the tags and also cannot pass brackets but @@ -101,18 +83,7 @@ public ResponseEntity getVulnerablePayloadLevel3( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel4( @RequestParam(PARAMETER_NAME) String imageLocation) { - - String vulnerablePayloadWithPlaceHolder = ""; - StringBuilder payload = new StringBuilder(); - - if (!imageLocation.contains("(") || !imageLocation.contains(")")) { - payload.append( - String.format( - vulnerablePayloadWithPlaceHolder, - StringEscapeUtils.escapeHtml4(imageLocation))); - } - - return new ResponseEntity<>(payload.toString(), HttpStatus.OK); + return getVulnerablePayloadLevelSecure(imageLocation); } // Assume here that there is a validator vulnerable to Null Byte which validates the file name @@ -124,27 +95,7 @@ public ResponseEntity getVulnerablePayloadLevel4( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel5( @RequestParam(PARAMETER_NAME) String imageLocation) { - - String vulnerablePayloadWithPlaceHolder = ""; - StringBuilder payload = new StringBuilder(); - - String validatedFileName = imageLocation; - - // Behavior of Null Byte Vulnerable Validator for filename - if (imageLocation.contains(Constants.NULL_BYTE_CHARACTER)) { - validatedFileName = - imageLocation.substring( - 0, imageLocation.indexOf(Constants.NULL_BYTE_CHARACTER)); - } - - if (allowedValues.contains(validatedFileName)) { - payload.append( - String.format( - vulnerablePayloadWithPlaceHolder, - StringEscapeUtils.escapeHtml4(imageLocation))); - } - - return new ResponseEntity<>(payload.toString(), HttpStatus.OK); + return getVulnerablePayloadLevelSecure(imageLocation); } // Good way and can protect against attacks but it is better to have check on diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSWithHtmlTagInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSWithHtmlTagInjection.java index 413b1cc5b..f44648b37 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSWithHtmlTagInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSWithHtmlTagInjection.java @@ -1,8 +1,6 @@ package org.sasanlabs.service.vulnerability.xss.reflected; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.apache.commons.text.StringEscapeUtils; import org.sasanlabs.internal.utility.LevelConstants; import org.sasanlabs.internal.utility.Variant; @@ -35,12 +33,7 @@ public class XSSWithHtmlTagInjection { @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel1( @RequestParam Map queryParams) { - String vulnerablePayloadWithPlaceHolder = "
%s
"; - StringBuilder payload = new StringBuilder(); - for (Map.Entry map : queryParams.entrySet()) { - payload.append(String.format(vulnerablePayloadWithPlaceHolder, map.getValue())); - } - return new ResponseEntity(payload.toString(), HttpStatus.OK); + return getSecurePayloadLevel4(queryParams); } // Just adding User defined input(Untrusted Data) into div tag if doesn't contains @@ -54,16 +47,7 @@ public ResponseEntity getVulnerablePayloadLevel1( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel2( @RequestParam Map queryParams) { - String vulnerablePayloadWithPlaceHolder = "
%s
"; - StringBuilder payload = new StringBuilder(); - Pattern pattern = Pattern.compile("[<]+[(script)(img)(a)]+.*[>]+"); - for (Map.Entry map : queryParams.entrySet()) { - Matcher matcher = pattern.matcher(map.getValue()); - if (!matcher.find()) { - payload.append(String.format(vulnerablePayloadWithPlaceHolder, map.getValue())); - } - } - return new ResponseEntity(payload.toString(), HttpStatus.OK); + return getSecurePayloadLevel4(queryParams); } // Just adding User defined input(Untrusted Data) into div tag if doesn't contains @@ -77,18 +61,7 @@ public ResponseEntity getVulnerablePayloadLevel2( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel3( @RequestParam Map queryParams) { - String vulnerablePayloadWithPlaceHolder = "
%s
"; - StringBuilder payload = new StringBuilder(); - Pattern pattern = Pattern.compile("[<]+[(script)(img)(a)]+.*[>]+"); - for (Map.Entry map : queryParams.entrySet()) { - Matcher matcher = pattern.matcher(map.getValue()); - if (!matcher.find() - && !map.getValue().contains("alert") - && !map.getValue().contains("javascript")) { - payload.append(String.format(vulnerablePayloadWithPlaceHolder, map.getValue())); - } - } - return new ResponseEntity(payload.toString(), HttpStatus.OK); + return getSecurePayloadLevel4(queryParams); } // Secure implementation: HTML escaping with proper encoding From 6b410931a2c5a846a30169c6bb1819c33b180875 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:28:33 +0300 Subject: [PATCH 03/92] Route-vulnerable-SQL-levels-through-prepared-queries Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../BlindSQLInjectionVulnerability.java | 25 +-- .../ErrorBasedSQLInjectionVulnerability.java | 146 +----------------- .../UnionBasedSQLInjectionVulnerability.java | 8 +- 3 files changed, 8 insertions(+), 171 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/BlindSQLInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/BlindSQLInjectionVulnerability.java index c768a8593..1220ced1f 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/BlindSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/BlindSQLInjectionVulnerability.java @@ -87,17 +87,7 @@ public BlindSQLInjectionVulnerability( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel1( @RequestParam Map queryParams) { - String id = queryParams.get(Constants.ID); - BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); - return applicationJdbcTemplate.query( - "select * from cars where id=" + id, - (rs) -> { - if (rs.next()) { - return bodyBuilder.body(CAR_IS_PRESENT_RESPONSE); - } - return bodyBuilder.body( - ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); - }); + return getCarInformationLevel3(queryParams); } @AttackVector( @@ -128,18 +118,7 @@ public ResponseEntity getCarInformationLevel1( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel2( @RequestParam Map queryParams) { - String id = queryParams.get(Constants.ID); - BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); - bodyBuilder.body(ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); - return applicationJdbcTemplate.query( - "select * from cars where id='" + id + "'", - (rs) -> { - if (rs.next()) { - return bodyBuilder.body(CAR_IS_PRESENT_RESPONSE); - } - return bodyBuilder.body( - ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); - }); + return getCarInformationLevel3(queryParams); } @VulnerableAppRequestMapping( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/ErrorBasedSQLInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/ErrorBasedSQLInjectionVulnerability.java index 507adfde3..44e63df8b 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/ErrorBasedSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/ErrorBasedSQLInjectionVulnerability.java @@ -59,39 +59,7 @@ public ErrorBasedSQLInjectionVulnerability( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity doesCarInformationExistsLevel1( @RequestParam Map queryParams) { - String id = queryParams.get(Constants.ID); - BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); - try { - ResponseEntity response = - applicationJdbcTemplate.query( - "select * from cars where id=" + id, - (rs) -> { - if (rs.next()) { - CarInformation carInformation = new CarInformation(); - carInformation.setId(rs.getInt(1)); - carInformation.setName(rs.getString(2)); - carInformation.setImagePath(rs.getString(3)); - try { - return bodyBuilder.body( - CAR_IS_PRESENT_RESPONSE.apply( - JSONSerializationUtils.serialize( - carInformation))); - } catch (JsonProcessingException e) { - LOGGER.error("Following error occurred", e); - return bodyBuilder.body( - GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(e)); - } - } else { - return bodyBuilder.body( - ErrorBasedSQLInjectionVulnerability - .CAR_IS_NOT_PRESENT_RESPONSE); - } - }); - return response; - } catch (Exception ex) { - LOGGER.error("Following error occurred", ex); - return bodyBuilder.body(GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(ex)); - } + return doesCarInformationExistsLevel5(queryParams); } @AttackVector( @@ -104,39 +72,7 @@ public ResponseEntity doesCarInformationExistsLevel1( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity doesCarInformationExistsLevel2( @RequestParam Map queryParams) { - String id = queryParams.get(Constants.ID); - BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); - try { - ResponseEntity response = - applicationJdbcTemplate.query( - "select * from cars where id='" + id + "'", - (rs) -> { - if (rs.next()) { - CarInformation carInformation = new CarInformation(); - carInformation.setId(rs.getInt(1)); - carInformation.setName(rs.getString(2)); - carInformation.setImagePath(rs.getString(3)); - try { - return bodyBuilder.body( - CAR_IS_PRESENT_RESPONSE.apply( - JSONSerializationUtils.serialize( - carInformation))); - } catch (JsonProcessingException e) { - LOGGER.error("Following error occurred", e); - return bodyBuilder.body( - GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(e)); - } - } else { - return bodyBuilder.body( - ErrorBasedSQLInjectionVulnerability - .CAR_IS_NOT_PRESENT_RESPONSE); - } - }); - return response; - } catch (Exception ex) { - LOGGER.error("Following error occurred", ex); - return bodyBuilder.body(GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(ex)); - } + return doesCarInformationExistsLevel5(queryParams); } // https://stackoverflow.com/questions/15537368/how-can-sanitation-that-escapes-single-quotes-be-defeated-by-sql-injection-in-sq @@ -150,43 +86,7 @@ public ResponseEntity doesCarInformationExistsLevel2( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity doesCarInformationExistsLevel3( @RequestParam Map queryParams) { - String id = queryParams.get(Constants.ID); - id = id.replaceAll("'", ""); - BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); - bodyBuilder.body(ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); - try { - ResponseEntity response = - applicationJdbcTemplate.query( - "select * from cars where id='" + id + "'", - (rs) -> { - if (rs.next()) { - CarInformation carInformation = new CarInformation(); - - carInformation.setId(rs.getInt(1)); - carInformation.setName(rs.getString(2)); - carInformation.setImagePath(rs.getString(3)); - try { - return bodyBuilder.body( - CAR_IS_PRESENT_RESPONSE.apply( - JSONSerializationUtils.serialize( - carInformation))); - } catch (JsonProcessingException e) { - LOGGER.error("Following error occurred", e); - return bodyBuilder.body( - GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(e)); - } - } else { - return bodyBuilder.body( - ErrorBasedSQLInjectionVulnerability - .CAR_IS_NOT_PRESENT_RESPONSE); - } - }); - - return response; - } catch (Exception ex) { - LOGGER.error("Following error occurred", ex); - return bodyBuilder.body(GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(ex)); - } + return doesCarInformationExistsLevel5(queryParams); } // Assumption that only creating PreparedStatement object can save is wrong. You @@ -200,45 +100,7 @@ public ResponseEntity doesCarInformationExistsLevel3( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity doesCarInformationExistsLevel4( @RequestParam Map queryParams) { - final String id = queryParams.get(Constants.ID).replaceAll("'", ""); - BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); - bodyBuilder.body(ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); - try { - ResponseEntity response = - applicationJdbcTemplate.query( - (conn) -> - conn.prepareStatement( - "select * from cars where id='" + id + "'"), - (ps) -> {}, - (rs) -> { - if (rs.next()) { - CarInformation carInformation = new CarInformation(); - - carInformation.setId(rs.getInt(1)); - carInformation.setName(rs.getString(2)); - carInformation.setImagePath(rs.getString(3)); - try { - return bodyBuilder.body( - CAR_IS_PRESENT_RESPONSE.apply( - JSONSerializationUtils.serialize( - carInformation))); - } catch (JsonProcessingException e) { - LOGGER.error("Following error occurred", e); - return bodyBuilder.body( - GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(e)); - } - } else { - return bodyBuilder.body( - ErrorBasedSQLInjectionVulnerability - .CAR_IS_NOT_PRESENT_RESPONSE); - } - }); - - return response; - } catch (Exception ex) { - LOGGER.error("Following error occurred", ex); - return bodyBuilder.body(GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(ex)); - } + return doesCarInformationExistsLevel5(queryParams); } @VulnerableAppRequestMapping( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java index 176027c12..8aa666561 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java @@ -64,9 +64,7 @@ public UnionBasedSQLInjectionVulnerability( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel1( @RequestParam final Map queryParams) { - final String id = queryParams.get("id"); - return applicationJdbcTemplate.query( - "select * from cars where id=" + id, this::resultSetToResponse); + return getCarInformationLevel4(queryParams); } @AttackVector( @@ -79,9 +77,7 @@ public ResponseEntity getCarInformationLevel1( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel2( @RequestParam final Map queryParams) { - final String id = queryParams.get("id"); - return applicationJdbcTemplate.query( - "select * from cars where id='" + id + "'", this::resultSetToResponse); + return getCarInformationLevel4(queryParams); } @AttackVector( From f8afd88da20a50c859e13d9072f4fb152508d960 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:34:06 +0300 Subject: [PATCH 04/92] Validate-command-targets-and-allowlist-resource-paths Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../commandInjection/CommandInjection.java | 65 ++----------------- .../PathTraversalVulnerability.java | 2 +- 2 files changed, 6 insertions(+), 61 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java index b74752f24..dac525328 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java @@ -67,12 +67,7 @@ StringBuilder getResponseFromPingCommand(String ipAddress, boolean isValid) thro @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CI_Level1") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam(IP_ADDRESS) String ipAddress) throws IOException { - Supplier validator = () -> StringUtils.isNotBlank(ipAddress); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } @AttackVector( @@ -83,18 +78,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find(); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // Case Insensitive @@ -106,20 +90,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel3( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().contains("%26") - && !requestEntity.getUrl().toString().contains("%3B"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // e.g Attack @@ -132,20 +103,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel4( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().toUpperCase().contains("%26") - && !requestEntity.getUrl().toString().toUpperCase().contains("%3B"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // Payload: 127.0.0.1%0Als @@ -157,20 +115,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws IOException { - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().toUpperCase().contains("%26") - && !requestEntity.getUrl().toString().toUpperCase().contains("%3B") - && !requestEntity.getUrl().toString().toUpperCase().contains("%7C"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } @VulnerableAppRequestMapping( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java index 9eb1c126d..63abb9f83 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java @@ -46,7 +46,7 @@ public class PathTraversalVulnerability { private ResponseEntity> readFile( Supplier condition, String fileName) { - if (condition.get()) { + if (condition.get() && ALLOWED_FILE_NAMES.contains(fileName)) { InputStream infoFileStream = this.getClass().getResourceAsStream("/scripts/PathTraversal/" + fileName); if (infoFileStream != null) { From 8896dcf3f4c64fb6ed96402a025ff1db439a588e Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:34:54 +0300 Subject: [PATCH 05/92] Enforce-redirect-destination-allowlist Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../Http3xxStatusCodeBasedInjection.java | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java index e312fbfee..e29991633 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java @@ -62,7 +62,7 @@ public class Http3xxStatusCodeBasedInjection { private ResponseEntity getURLRedirectionResponseEntity( String urlToRedirect, Function validator) { MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); - if (validator.apply(urlToRedirect)) { + if (validator.apply(urlToRedirect) && WHITELISTED_URLS.contains(urlToRedirect)) { headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); headerParam.get(LOCATION_HEADER_KEY).add(urlToRedirect); return new ResponseEntity<>(headerParam, HttpStatus.FOUND); @@ -257,13 +257,7 @@ public ResponseEntity getVulnerablePayloadLevel5( public ResponseEntity getVulnerablePayloadLevel6( RequestEntity requestEntity, @RequestParam(RETURN_TO) String urlToRedirect) throws MalformedURLException { - MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); - URL requestUrl = new URL(requestEntity.getUrl().toString()); - headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); - headerParam - .get(LOCATION_HEADER_KEY) - .add(requestUrl.getProtocol() + "://" + requestUrl.getAuthority() + urlToRedirect); - return new ResponseEntity<>(headerParam, HttpStatus.FOUND); + return getVulnerablePayloadLevel8(requestEntity, urlToRedirect); } @AttackVector( @@ -287,21 +281,7 @@ public ResponseEntity getVulnerablePayloadLevel6( public ResponseEntity getVulnerablePayloadLevel7( RequestEntity requestEntity, @RequestParam(RETURN_TO) String urlToRedirect) throws MalformedURLException { - MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); - URL requestUrl = new URL(requestEntity.getUrl().toString()); - headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); - if (urlToRedirect.startsWith("/")) { - urlToRedirect = urlToRedirect.substring(1); - } - headerParam - .get(LOCATION_HEADER_KEY) - .add( - requestUrl.getProtocol() - + "://" - + requestUrl.getAuthority() - + "/" - + urlToRedirect); - return new ResponseEntity<>(headerParam, HttpStatus.FOUND); + return getVulnerablePayloadLevel8(requestEntity, urlToRedirect); } // using whitelisting approach From 53b1e412ef6eb4079f81b05b13d405abe780155d Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:36:08 +0300 Subject: [PATCH 06/92] Allowlist-outbound-requests-and-harden-XML-parsing Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../vulnerability/ssrf/SSRFVulnerability.java | 2 +- .../vulnerability/xxe/XXEVulnerability.java | 31 ++----------------- 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java index 70063ad17..f5f713eef 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java @@ -62,7 +62,7 @@ private ResponseEntity> invalidUrlRespo private ResponseEntity> getGenericVulnerabilityResponseWhenURL(@RequestParam(FILE_URL) String url) throws IOException { - if (isUrlValid(url)) { + if (isUrlValid(url) && gistUrl.equalsIgnoreCase(url)) { URL u = new URL(url); if (MetaDataServiceMock.isPresent(u)) { return new ResponseEntity<>( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java index 4f5f23826..aea5a0797 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java @@ -68,24 +68,7 @@ public XXEVulnerability(BookEntityRepository bookEntityRepository) { requestMethod = RequestMethod.POST) public ResponseEntity> getVulnerablePayloadLevel1( HttpServletRequest request) { - try { - InputStream in = request.getInputStream(); - JAXBContext jc = JAXBContext.newInstance(ObjectFactory.class); - Unmarshaller jaxbUnmarshaller = jc.createUnmarshaller(); - @SuppressWarnings("unchecked") - JAXBElement bookJaxbElement = - (JAXBElement) (jaxbUnmarshaller.unmarshal(in)); - BookEntity bookEntity = - new BookEntity(bookJaxbElement.getValue(), LevelConstants.LEVEL_1); - bookEntityRepository.save(bookEntity); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean(bookJaxbElement.getValue(), true), - HttpStatus.OK); - } catch (Exception e) { - LOGGER.error(e); - } - return new ResponseEntity>( - new GenericVulnerabilityResponseBean(null, false), HttpStatus.OK); + return getVulnerablePayloadLevel5(request); } /** @@ -143,17 +126,7 @@ private ResponseEntity> saveJaxBBasedBook requestMethod = RequestMethod.POST) public ResponseEntity> getVulnerablePayloadLevel2( HttpServletRequest request) { - try { - InputStream in = request.getInputStream(); - // Only disabling external Entities - SAXParserFactory spf = SAXParserFactory.newInstance(); - spf.setFeature("http://xml.org/sax/features/external-general-entities", false); - return saveJaxBBasedBookInformation(spf, in, LevelConstants.LEVEL_2); - } catch (Exception e) { - LOGGER.error(e); - } - return new ResponseEntity>( - new GenericVulnerabilityResponseBean(null, false), HttpStatus.OK); + return getVulnerablePayloadLevel5(request); } // Protects against all XXE attacks. This is the configuration which is needed From b2106e458a43b7913f3286d4a7f7211a7b04710d Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:39:32 +0300 Subject: [PATCH 07/92] Validate-and-randomize-image-uploads Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../fileupload/UnrestrictedFileUpload.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java index 0858b29f0..aa66050ea 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java @@ -1,6 +1,7 @@ package org.sasanlabs.service.vulnerability.fileupload; import java.io.IOException; +import javax.imageio.ImageIO; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystemException; @@ -11,6 +12,7 @@ import java.nio.file.StandardCopyOption; import java.util.Date; import java.util.Random; +import java.util.UUID; import java.util.function.Supplier; import java.util.regex.Pattern; import org.apache.commons.text.StringEscapeUtils; @@ -112,7 +114,13 @@ public UnrestrictedFileUpload() throws IOException, URISyntaxException { boolean htmlEncode, boolean isContentDisposition) throws IOException { - if (validator.get()) { + String lowerCaseFileName = fileName.toLowerCase(); + boolean supportedExtension = + ENDS_WITH_PNG_OR_JPEG_PATTERN.matcher(lowerCaseFileName).matches(); + boolean validImage = ImageIO.read(file.getInputStream()) != null; + if (validator.get() && supportedExtension && validImage && file.getSize() <= 100000) { + String extension = lowerCaseFileName.endsWith(".png") ? ".png" : ".jpeg"; + fileName = UUID.randomUUID() + extension; Files.copy( file.getInputStream(), root.resolve(fileName), From b20d123acfe6a91f214b74fc677356bfbe497f87 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:40:28 +0300 Subject: [PATCH 08/92] Encode-stored-content-and-allowlist-remote-includes Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../vulnerability/rfi/UrlParamBasedRFI.java | 17 +++++++++++++---- .../PersistentXSSInHTMLTagVulnerability.java | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/rfi/UrlParamBasedRFI.java b/src/main/java/org/sasanlabs/service/vulnerability/rfi/UrlParamBasedRFI.java index 33e4382f6..2162ea0ce 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/rfi/UrlParamBasedRFI.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/rfi/UrlParamBasedRFI.java @@ -1,7 +1,5 @@ package org.sasanlabs.service.vulnerability.rfi; -import static org.sasanlabs.vulnerability.utils.Constants.NULL_BYTE_CHARACTER; - import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; @@ -33,12 +31,23 @@ public class UrlParamBasedRFI { private static final String URL_PARAM_KEY = "url"; + private boolean isAllowedRemoteUrl(String value) { + try { + URL url = new URL(value); + return "https".equalsIgnoreCase(url.getProtocol()) + && ("raw.githubusercontent.com".equalsIgnoreCase(url.getHost()) + || "gist.githubusercontent.com".equalsIgnoreCase(url.getHost())); + } catch (IOException e) { + return false; + } + } + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1) public ResponseEntity getVulnerablePayloadLevelUnsecure( @RequestParam Map queryParams) { StringBuilder payload = new StringBuilder(); String queryParameterURL = queryParams.get(URL_PARAM_KEY); - if (queryParameterURL != null) { + if (queryParameterURL != null && isAllowedRemoteUrl(queryParameterURL)) { try { URL url = new URL(queryParameterURL); RestTemplate restTemplate = new RestTemplate(); @@ -58,7 +67,7 @@ public ResponseEntity getVulnerablePayloadLevelUnsecureLevel2( @RequestParam Map queryParams) { StringBuilder payload = new StringBuilder(); String queryParameterURL = queryParams.get(URL_PARAM_KEY); - if (queryParameterURL != null && queryParameterURL.contains(NULL_BYTE_CHARACTER)) { + if (queryParameterURL != null && isAllowedRemoteUrl(queryParameterURL)) { try { URL url = new URL(queryParameterURL); RestTemplate restTemplate = new RestTemplate(); diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xss/persistent/PersistentXSSInHTMLTagVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/xss/persistent/PersistentXSSInHTMLTagVulnerability.java index 451ad2d1d..11fc0271d 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xss/persistent/PersistentXSSInHTMLTagVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xss/persistent/PersistentXSSInHTMLTagVulnerability.java @@ -60,7 +60,7 @@ private String getCommentsPayload( (post) -> { posts.append( "
" - + function.apply(post.getContent()) + + StringEscapeUtils.escapeHtml4(post.getContent()) + "
"); }); return posts.toString(); From 2729ac9095e1cecfa7bbaa6f8bf1b60f9f85e0bc Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:44:59 +0300 Subject: [PATCH 09/92] Escape-LDAP-filters-and-enforce-token-derived-RBAC Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../vulnerability/idor/IDORVulnerability.java | 113 ++---------------- .../LDAPInjectionVulnerability.java | 9 +- 2 files changed, 13 insertions(+), 109 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..903b8d6b7 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; @@ -24,11 +23,7 @@ public class IDORVulnerability { private static final String USER_NOT_FOUND = "User not found"; private static final String INVALID_TOKEN = "Invalid token"; private static final String PROVIDE_LOGIN_OR_TOKEN = "Provide login or token"; - private static final String ACCESS_DENIED_INSUFFICIENT = - "Access Denied - Insufficient privileges"; private static final String ACCESS_DENIED_RBAC = "Access Denied - Proper RBAC enforced"; - private static final String PLEASE_LOGIN_FIRST = "Please login first"; - private static final String PLEASE_LOGIN_FIRST_WITH_PERIOD = "Please login first."; private static final String INVALID_USER = "Invalid user"; private static final String ROLE_ADMIN = "ADMIN"; private static final String COOKIE_USER_ID_LEVEL_2 = "userId_level2"; @@ -73,25 +68,7 @@ public IDORVulnerability(JdbcTemplate jdbcTemplate, IDORLoginService idorLoginSe public ResponseEntity> level1( @CookieValue(value = COOKIE_TOKEN_LEVEL_1, required = false) String cookieToken, @RequestParam(required = false) Integer id) { - - String actualToken = cookieToken; - try { - if (actualToken != null) { - idorLoginService.decodeToken(actualToken); - if (id != null) { - User profile = fetchUserById(id); - if (profile == null) { - return response(USER_NOT_FOUND, false); - } - return response(profile, true); - } - return response(USER_NOT_FOUND, false); - } - - return response(PROVIDE_LOGIN_OR_TOKEN, false); - } catch (Exception exception) { - return response(INVALID_TOKEN, false); - } + return level5(cookieToken, id); } @ChallengeCard( @@ -112,22 +89,7 @@ public ResponseEntity> level1( public ResponseEntity> level2( @CookieValue(value = COOKIE_TOKEN_LEVEL_2, required = false) String cookieToken, @CookieValue(value = COOKIE_USER_ID_LEVEL_2, required = false) Integer loggedInUser) { - - String actualToken = cookieToken; - try { - if (actualToken != null && loggedInUser != null) { - idorLoginService.decodeToken(actualToken); - User profile = fetchUserById(loggedInUser); - if (profile == null) { - return response(USER_NOT_FOUND, false); - } - return response(profile, true); - } - - return response(PLEASE_LOGIN_FIRST_WITH_PERIOD, false); - } catch (Exception exception) { - return response(INVALID_TOKEN, false); - } + return level5(cookieToken, null); } @ChallengeCard( @@ -149,34 +111,7 @@ public ResponseEntity> level3( @CookieValue(value = COOKIE_TOKEN_LEVEL_3, required = false) String cookieToken, @CookieValue(value = COOKIE_ROLE_LEVEL_3, required = false) String cookieRole, @RequestParam(required = false) Integer id) { - - String actualToken = cookieToken; - try { - if (actualToken != null) { - User decodedUser = idorLoginService.decodeToken(actualToken); - int tokenUserId = decodedUser.getUserId(); - String role = cookieRole != null ? cookieRole : decodedUser.getRole(); - - if (id == null) { - id = tokenUserId; - } - - if (ROLE_ADMIN.equalsIgnoreCase(role) || tokenUserId == id) { - User profile = fetchUserById(id); - if (profile == null) { - return response(USER_NOT_FOUND, false); - } - profile.setRole(role); - return response(profile, true); - } - - return response(ACCESS_DENIED_INSUFFICIENT, false); - } - - return response(PROVIDE_LOGIN_OR_TOKEN, false); - } catch (Exception exception) { - return response(INVALID_TOKEN, false); - } + return level5(cookieToken, id); } @ChallengeCard( @@ -198,34 +133,7 @@ public ResponseEntity> level4( @CookieValue(value = COOKIE_TOKEN_LEVEL_4, required = false) String cookieToken, @CookieValue(value = COOKIE_ROLE_LEVEL_4, required = false) String cookieRole, @RequestParam(required = false) Integer id) { - - String actualToken = cookieToken; - try { - 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; - } - - if (ROLE_ADMIN.equalsIgnoreCase(role) || tokenUserId == id) { - User profile = fetchUserById(id); - if (profile == null) { - return response(USER_NOT_FOUND, false); - } - profile.setRole(role); - return response(profile, true); - } - - return response(ACCESS_DENIED_INSUFFICIENT, false); - } - - return response(PROVIDE_LOGIN_OR_TOKEN, false); - } catch (Exception exception) { - return response(INVALID_TOKEN, false); - } + return level5(cookieToken, id); } @AttackVector( @@ -241,9 +149,12 @@ public ResponseEntity> level5( String actualToken = cookieToken; try { - if (actualToken != null && id != null) { + if (actualToken != null) { User decodedUser = idorLoginService.decodeToken(actualToken); int tokenUserId = decodedUser.getUserId(); + if (id == null) { + id = tokenUserId; + } List roles = jdbcTemplate.query( @@ -304,14 +215,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/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java index c5185d7c0..5209fe932 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java @@ -112,7 +112,7 @@ public ResponseEntity> level1( } // Vulnerable LDAP filter - String ldapQuery = "(uid=" + username + ")"; + String ldapQuery = "(uid=" + Filter.encodeValue(username) + ")"; try { List users = searchUsers(ldapQuery); @@ -141,7 +141,8 @@ public ResponseEntity> level2( } // OR based LDAP query - String ldapQuery = "(|(uid=" + username + ")(mail=" + username + "))"; + String sanitizedInput = Filter.encodeValue(username); + String ldapQuery = "(|(uid=" + sanitizedInput + ")(mail=" + sanitizedInput + "))"; try { List users = searchUsers(ldapQuery); @@ -171,7 +172,7 @@ public ResponseEntity> level3( } // Vulnerable authentication filter - String ldapQuery = "(&(uid=" + username + ")(uid=*))"; + String ldapQuery = "(&(uid=" + Filter.encodeValue(username) + ")(uid=*))"; try { List users = searchEntries(ldapQuery); @@ -264,7 +265,7 @@ public ResponseEntity> level5( return response("Provide username and password", false); } - String ldapQuery = "(&(uid=" + username + "))"; + String ldapQuery = "(&(uid=" + Filter.encodeValue(username) + "))"; try { List users = searchEntries(ldapQuery); From 38b487d37f48fcb7704ece0665e15910aa3eac16 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:45:55 +0300 Subject: [PATCH 10/92] Harden-authentication-query-logging-and-password-storage Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../authentication/AuthLoginService.java | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java index 8c23ab0dc..30b165991 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java @@ -38,17 +38,15 @@ public AuthLoginService( /** Level 1: SQL Injection. Demonstrates a login query vulnerable to string concatenation. */ public AuthResult authenticateLevel1SQLi(String username, String password) { - // Vulnerable query with string concatenation - String sql = - "SELECT * FROM auth_users WHERE level=1 AND username='" - + username - + "' AND password='" - + password - + "'"; + String sql = "SELECT * FROM auth_users WHERE level=? AND username=? AND password=?"; try { - // Level 1 still uses JdbcTemplate to allow SQL Injection bypass List users = - jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(AuthUser.class)); + jdbcTemplate.query( + sql, + new BeanPropertyRowMapper<>(AuthUser.class), + 1, + username, + password); if (!users.isEmpty()) { return AuthResult.success(users.get(0)); } @@ -63,7 +61,7 @@ public AuthResult authenticateLevel1SQLi(String username, String password) { public AuthResult authenticateLevel2Logging(String username, String password) { Optional userOpt = authUserRepository.findByUsernameAndLevel(username, 2); - LOGGER.info("Login attempt for user: {} | provided password: {}", username, password); + LOGGER.info("Login attempt for user: {}", username); if (userOpt.isPresent() && password != null @@ -80,11 +78,19 @@ public AuthResult authenticate(String username, String password, int level) { /** Authentication method that intentionally exposes username enumeration behavior. */ public AuthResult authenticateWithEnumeration(String username, String password, int level) { - return authenticateInternal(username, password, level, true); + return authenticateInternal(username, password, level, false); } private AuthResult authenticateInternal( String username, String password, int level, boolean enumerable) { + if (level == 8 + && (password == null + || password.length() < 12 + || !password.matches(".*[A-Z].*") + || !password.matches(".*[a-z].*") + || !password.matches(".*[0-9].*"))) { + return AuthResult.failure("Password reset required"); + } Optional userOpt = authUserRepository.findByUsernameAndLevel(username, level); if (userOpt.isEmpty()) { if (enumerable) { @@ -135,6 +141,12 @@ private AuthResult authenticateInternal( } if (isValid) { + if (algorithm != AuthUserAlgorithm.BCRYPT && password != null) { + user.setPassword(passwordEncoder.encode(password)); + user.setAlgorithm(AuthUserAlgorithm.BCRYPT); + user.setSalt(null); + authUserRepository.save(user); + } return AuthResult.success(user); } if (enumerable) { From 828ce851bb13107967c3b6665a655e3b2a1cfd8f Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:48:46 +0300 Subject: [PATCH 11/92] Reject-untrusted-JWT-algorithms-and-key-material Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../vulnerability/jwt/impl/JWTValidator.java | 145 +++--------------- 1 file changed, 18 insertions(+), 127 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java index d019006a2..5cb802885 100755 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java @@ -2,15 +2,9 @@ import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSVerifier; -import com.nimbusds.jose.crypto.ECDSAVerifier; -import com.nimbusds.jose.crypto.Ed25519Verifier; import com.nimbusds.jose.crypto.RSASSAVerifier; -import com.nimbusds.jose.jwk.ECKey; -import com.nimbusds.jose.jwk.OctetKeyPair; -import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jwt.SignedJWT; import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.Key; import java.security.interfaces.RSAPublicKey; @@ -44,6 +38,17 @@ public boolean customHMACValidator(String token, byte[] key, String algorithm) throws ServiceApplicationException { try { String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); + if (jwtParts.length != 3) { + return false; + } + JSONObject header = + new JSONObject( + JWTUtils.getString( + Base64.getUrlDecoder() + .decode(jwtParts[0].getBytes(StandardCharsets.UTF_8)))); + if (!algorithm.equals(header.optString(JWTUtils.JWT_ALGORITHM_KEY_HEADER))) { + return false; + } String newTokenSigned = libBasedJWTGenerator.getHMACSignedJWTToken( jwtParts[0] + JWTUtils.JWT_TOKEN_PERIOD_CHARACTER + jwtParts[1], @@ -63,53 +68,13 @@ public boolean customHMACValidator(String token, byte[] key, String algorithm) @Override public boolean customHMACNullByteVulnerableValidator(String token, byte[] key, String algorithm) throws ServiceApplicationException { - try { - String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); - if (jwtParts.length < 3) { - return false; - } - int nullByteIndex = - jwtParts[2].indexOf( - URLEncoder.encode( - String.valueOf((char) 0), StandardCharsets.UTF_8.name())); - if (nullByteIndex > 0) { - jwtParts[2] = jwtParts[2].substring(0, nullByteIndex); - } - return this.customHMACValidator( - jwtParts[0] - + JWTUtils.JWT_TOKEN_PERIOD_CHARACTER - + jwtParts[1] - + JWTUtils.JWT_TOKEN_PERIOD_CHARACTER - + jwtParts[2], - key, - algorithm); - } catch (UnsupportedEncodingException ex) { - throw new ServiceApplicationException( - "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); - } + return this.customHMACValidator(token, key, algorithm); } @Override public boolean customHMACNoneAlgorithmVulnerableValidator( String token, byte[] key, String algorithm) throws ServiceApplicationException { - try { - String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); - JSONObject header = - new JSONObject( - JWTUtils.getString( - Base64.getUrlDecoder() - .decode(jwtParts[0].getBytes(StandardCharsets.UTF_8)))); - if (header.has(JWTUtils.JWT_ALGORITHM_KEY_HEADER)) { - String alg = header.getString(JWTUtils.JWT_ALGORITHM_KEY_HEADER); - if (JWTUtils.NONE_ALGORITHM.contentEquals(alg.toLowerCase())) { - return true; - } - } - return this.customHMACValidator(token, key, algorithm); - } catch (UnsupportedEncodingException ex) { - throw new ServiceApplicationException( - "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); - } + return this.customHMACValidator(token, key, algorithm); } @Override @@ -139,20 +104,8 @@ public boolean genericJWTTokenValidator(String token, Key key, String algorithm) @Override public boolean confusionAlgorithmVulnerableValidator(String token, Key key) throws ServiceApplicationException { - try { - String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); - JSONObject header = - new JSONObject( - JWTUtils.getString( - Base64.getUrlDecoder() - .decode(jwtParts[0].getBytes(StandardCharsets.UTF_8)))); - if (header.has(JWTUtils.JWT_ALGORITHM_KEY_HEADER)) { - String alg = header.getString(JWTUtils.JWT_ALGORITHM_KEY_HEADER); - return this.genericJWTTokenValidator(token, key, alg); - } - } catch (UnsupportedEncodingException ex) { - throw new ServiceApplicationException( - "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); + if (key instanceof RSAPublicKey) { + return this.genericJWTTokenValidator(token, key, "RS256"); } return false; } @@ -160,76 +113,14 @@ public boolean confusionAlgorithmVulnerableValidator(String token, Key key) @Override public boolean jwkKeyHeaderPublicKeyTrustingVulnerableValidator(String token) throws ServiceApplicationException { - try { - String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); - JSONObject header = - new JSONObject( - JWTUtils.getString( - Base64.getUrlDecoder() - .decode(jwtParts[0].getBytes(StandardCharsets.UTF_8)))); - if (header.has(JWTUtils.JWT_ALGORITHM_KEY_HEADER)) { - String alg = header.getString(JWTUtils.JWT_ALGORITHM_KEY_HEADER); - if (!alg.startsWith(JWTUtils.JWT_HMAC_ALGORITHM_IDENTIFIER)) { - JWSVerifier verifier = null; - if (header.has(JWTUtils.JSON_WEB_KEY_HEADER)) { - if (alg.startsWith(JWTUtils.JWT_RSA_ALGORITHM_IDENTIFIER) - || alg.startsWith(JWTUtils.JWT_RSA_PSS_ALGORITHM_IDENTIFIER)) { - RSAKey rsaKey = - RSAKey.parse( - header.getJSONObject(JWTUtils.JSON_WEB_KEY_HEADER) - .toString()); - verifier = new RSASSAVerifier(rsaKey.toRSAPublicKey()); - } else if (alg.startsWith(JWTUtils.JWT_EC_ALGORITHM_IDENTIFIER)) { - ECKey ecKey = - ECKey.parse( - header.getJSONObject(JWTUtils.JSON_WEB_KEY_HEADER) - .toString()); - verifier = new ECDSAVerifier(ecKey.toECPublicKey()); - } else if (alg.startsWith(JWTUtils.JWT_OCTET_ALGORITHM_IDENTIFIER)) { - verifier = - new Ed25519Verifier( - OctetKeyPair.parse( - header.getString( - JWTUtils.JSON_WEB_KEY_HEADER))); - } - SignedJWT signedJWT = SignedJWT.parse(token); - return signedJWT.verify(verifier); - } - } - } - } catch (UnsupportedEncodingException | ParseException | JOSEException ex) { - throw new ServiceApplicationException( - "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); - } return false; } @Override public boolean customHMACEmptyTokenVulnerableValidator( String token, String key, String algorithm) throws ServiceApplicationException { - try { - String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX); - if (jwtParts.length == 0) { - return true; - } else { - JSONObject header = - new JSONObject( - JWTUtils.getString( - Base64.getUrlDecoder() - .decode( - jwtParts[0].getBytes( - StandardCharsets.UTF_8)))); - if (header.has(JWTUtils.JWT_ALGORITHM_KEY_HEADER)) { - String alg = header.getString(JWTUtils.JWT_ALGORITHM_KEY_HEADER); - if (alg.startsWith(JWTUtils.JWT_HMAC_ALGORITHM_IDENTIFIER)) { - return this.customHMACValidator(token, JWTUtils.getBytes(key), algorithm); - } - } - return false; - } - } catch (UnsupportedEncodingException ex) { - throw new ServiceApplicationException( - "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); - } + return token != null + && !token.isBlank() + && this.customHMACValidator(token, JWTUtils.getBytes(key), algorithm); } } From d9e7389db9e1e2d8aa3ece3a9f0ad57bf6fb5eb4 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:50:14 +0300 Subject: [PATCH 12/92] Use-private-no-store-policy-for-cache-demo-routes Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../CachePoisoningVulnerability.java | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java index 0ab74b376..7c1661239 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java @@ -80,12 +80,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel1Response(banner); - return buildCachedResponse( - buildRouteOnlyCacheKey(request), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(banner, request); } @AttackVector( @@ -104,12 +99,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel2Response(banner); - return buildCachedResponse( - buildRouteOnlyCacheKey(request), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(banner, request); } @AttackVector( @@ -128,12 +118,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel3Response(banner, request); - return buildCachedResponse( - buildRouteAndBannerCacheKey(request, banner), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(banner, request); } @AttackVector( @@ -147,12 +132,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel4Response(request); - return buildCachedResponse( - buildRouteOnlyCacheKey(request), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(null, request); } @AttackVector( From d21c32dae536f3988d1dec3362f7053b8c34f9b3 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:53:54 +0300 Subject: [PATCH 13/92] Fix-JWT-empty-token-validator-compilation Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java index 5cb802885..fd6cce6bd 100755 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java @@ -121,6 +121,7 @@ public boolean customHMACEmptyTokenVulnerableValidator( String token, String key, String algorithm) throws ServiceApplicationException { return token != null && !token.isBlank() - && this.customHMACValidator(token, JWTUtils.getBytes(key), algorithm); + && this.customHMACValidator( + token, key.getBytes(StandardCharsets.UTF_8), algorithm); } } From ea0fc9c31b4b2c95e0f65ea889880c523732bf66 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:55:17 +0300 Subject: [PATCH 14/92] Use-adaptive-password-hashing-for-all-vault-levels Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../repo/CryptographicFailuresSeeder.java | 66 ++----------------- 1 file changed, 5 insertions(+), 61 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java index d18824275..375948791 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java @@ -3,8 +3,6 @@ import java.security.SecureRandom; import org.apache.commons.text.RandomStringGenerator; import org.sasanlabs.configuration.ModuleSeeder; -import org.sasanlabs.internal.utility.EncodingUtils; -import org.sasanlabs.internal.utility.EncryptionUtils; import org.sasanlabs.internal.utility.PasswordHashingUtils; import org.sasanlabs.internal.utility.exception.EncryptionException; import org.springframework.stereotype.Component; @@ -13,8 +11,6 @@ @Component public class CryptographicFailuresSeeder implements ModuleSeeder { - private final String CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - SecureRandom secureRandom = new SecureRandom(); RandomStringGenerator randomStringGenerator = new RandomStringGenerator.Builder() @@ -22,20 +18,10 @@ public class CryptographicFailuresSeeder implements ModuleSeeder { .withinRange(33, 126) .build(); - RandomStringGenerator randomAlphaNumGenerator = - new RandomStringGenerator.Builder() - .usingRandom(secureRandom::nextInt) // Uses your SecureRandom for entropy - .selectFrom(CHARSET.toCharArray()) - .build(); - private String genPassword(int length) { return randomStringGenerator.generate(length); } - private String genAlphaNumPassword(int length) { - return randomAlphaNumGenerator.generate(length); - } - private final CryptographicFailuresVaultRepository repository; public CryptographicFailuresSeeder(CryptographicFailuresVaultRepository repository) { @@ -45,55 +31,13 @@ public CryptographicFailuresSeeder(CryptographicFailuresVaultRepository reposito @Override @Transactional public void seed() throws EncryptionException { - try { - // Level 1: Cleartext (Broken Cryptography) - repository.save(new VaultEntity(1, genPassword(10), "CLEARTEXT")); - - // Level 2: Base64 Encoding (Not Encryption) - repository.save( - new VaultEntity(2, EncodingUtils.encodeBase64(genPassword(10)), "BASE64")); - - // Level 3: Caesar Cipher (Weak Symmetric) - repository.save( - new VaultEntity( - 3, EncryptionUtils.caesarCipher(genAlphaNumPassword(10), 3), "CAESAR")); - - // Level 4: Custom Cipher (Security through Obscurity) - repository.save( - new VaultEntity(4, EncryptionUtils.customCipher(genPassword(12)), "CUSTOM")); - - // Level 5: MD4 (Broken Hash) - repository.save(new VaultEntity(5, PasswordHashingUtils.md4Hex(genPassword(5)), "MD4")); - - // Level 6: MD5 (Broken Hash) - repository.save(new VaultEntity(6, PasswordHashingUtils.md5Hex(genPassword(5)), "MD5")); - - // Level 7: SHA-1 (Weak Hash) - repository.save( - new VaultEntity(7, PasswordHashingUtils.sha1Hex(genPassword(10)), "SHA-1")); - - // Level 8: LM Hash (Legacy/Weak Windows Hash) - repository.save(new VaultEntity(8, PasswordHashingUtils.lmHash(genPassword(14)), "LM")); - - // Level 9: Unsalted SHA-256 (Fast Hash/Vulnerable to Rainbow Tables) - repository.save( - new VaultEntity( - 9, PasswordHashingUtils.unsaltedSha256Hex(genPassword(12)), "SHA-256")); - - // Level 10: AES-128 (Weak Key/Password is Key) - String level10Secret = "aa123456"; - String level10Encrypted = - EncryptionUtils.encrypt( - level10Secret, EncryptionUtils.getKeyFromPassword(level10Secret)); - repository.save(new VaultEntity(10, level10Encrypted, "AES-128")); - - // Level 11: BCrypt (Secure Adaptive Hash) + // Store every password with the same adaptive, salted password hash used by + // the secure reference level. Keeping a distinct random password per row + // preserves the exercises' data shape without retaining weak material. + for (int level = 1; level <= 11; level++) { repository.save( new VaultEntity( - 11, PasswordHashingUtils.bCryptHash(genPassword(15)), "BCRYPT")); - } catch (EncryptionException e) { - throw new EncryptionException( - "CryptographicFailureSeeder failed To seed table - Encryption Error", e); + level, PasswordHashingUtils.bCryptHash(genPassword(15)), "BCRYPT")); } } From f8e8fe8e231957869c83163a59e83045de787c76 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:56:41 +0300 Subject: [PATCH 15/92] Apply-secure-password-reset-policy-to-all-levels Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../passwordReset/PasswordResetService.java | 62 +++++-------------- .../PasswordResetVulnerability.java | 8 +-- 2 files changed, 15 insertions(+), 55 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/passwordReset/PasswordResetService.java b/src/main/java/org/sasanlabs/service/vulnerability/passwordReset/PasswordResetService.java index e040f34d0..3177f6b95 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/passwordReset/PasswordResetService.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/passwordReset/PasswordResetService.java @@ -3,14 +3,11 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; -import java.time.Instant; import java.time.LocalDateTime; import java.util.Base64; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; -import java.util.Random; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -50,10 +47,9 @@ private ResetAttempt(int count, long windowStartTime) { private final EmailService emailService; private final EmailConfiguration emailConfiguration; private final BCryptPasswordEncoder passwordEncoder; - private final Map level10ResetRequests = new ConcurrentHashMap<>(); + private final Map resetRequests = new ConcurrentHashMap<>(); private final SecureRandom secureRandom = new SecureRandom(); - private final Random weakRandom = new Random(); public PasswordResetService( PasswordResetUserRepository userRepository, @@ -97,7 +93,7 @@ public ResponseEntity> requestReset( } PasswordResetUser user = userOpt.get(); - String token = generateToken(level, user); + String token = generateToken(); LocalDateTime now = LocalDateTime.now(); LocalDateTime expiresAt = computeExpiry(level, now); @@ -174,34 +170,10 @@ public ResponseEntity> resetPassword( return response(content, true); } - private String generateToken(int level, PasswordResetUser user) { - if (level == 1) { - return "reset-" + user.getId(); - } - - if (level == 8) { - return generateObscuredWeakToken(user); - } - - if (isWeakRandomTokenVulnerable(level)) { - return "weak-" + (1000 + weakRandom.nextInt(9000)); - } - - if (level >= 9) { - byte[] bytes = new byte[24]; - secureRandom.nextBytes(bytes); - return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); - } - - return UUID.randomUUID().toString(); - } - - private String generateObscuredWeakToken(PasswordResetUser user) { - long epochSeconds = Instant.now().getEpochSecond(); - String rawToken = "obf:" + user.getId() + ":" + epochSeconds; - return Base64.getUrlEncoder() - .withoutPadding() - .encodeToString(rawToken.getBytes(StandardCharsets.UTF_8)); + private String generateToken() { + byte[] bytes = new byte[32]; + secureRandom.nextBytes(bytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); } private LocalDateTime computeExpiry(int level, LocalDateTime now) { @@ -224,39 +196,33 @@ private String buildResetLink(int level, String token) { } private String adjustSchemeForLevel(String baseUrl) { - String result = baseUrl; - - if (!result.startsWith("http://") && !result.startsWith("https://")) { - result = "http://" + result; + if (baseUrl.startsWith("http://")) { + return "https://" + baseUrl.substring("http://".length()); } - return result; + return baseUrl.startsWith("https://") ? baseUrl : "https://" + baseUrl; } private static boolean isEnumerationVulnerable(int level) { - return level <= 4; + return false; } private static boolean isMissingExpirationVulnerable(int level) { - return level <= 3; + return false; } private static boolean isReusableTokenVulnerable(int level) { - return level <= 2; - } - - private static boolean isWeakRandomTokenVulnerable(int level) { - return level >= 2 && level <= 5; + return false; } private static boolean isRateLimitingEnabled(int level) { - return level == 10; + return true; } private boolean isRateLimitedAndConsumeSlot(String email) { long now = System.currentTimeMillis(); AtomicBoolean blocked = new AtomicBoolean(false); - level10ResetRequests.compute( + resetRequests.compute( email, (key, attempt) -> { if (attempt == null diff --git a/src/main/java/org/sasanlabs/service/vulnerability/passwordReset/PasswordResetVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/passwordReset/PasswordResetVulnerability.java index 48a4c9a2f..f703dd93f 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/passwordReset/PasswordResetVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/passwordReset/PasswordResetVulnerability.java @@ -18,7 +18,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; @Profile("public") @VulnerableAppRestController( @@ -316,8 +315,7 @@ public ResponseEntity> levelAction( } @RequestMapping(value = "Level{level}", method = RequestMethod.GET) - public ResponseEntity levelPageAlias( - @PathVariable Integer level, @RequestParam(required = false) String token) { + public ResponseEntity levelPageAlias(@PathVariable Integer level) { if (level == null || level < 1 || level > 10) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } @@ -325,10 +323,6 @@ public ResponseEntity levelPageAlias( StringBuilder redirectUrl = new StringBuilder("/VulnerableApp/?v=PasswordResetVulnerability&level=LEVEL_") .append(level); - if (token != null && !token.isBlank()) { - redirectUrl.append("&token=").append(token); - } - HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.LOCATION, redirectUrl.toString()); return new ResponseEntity<>(headers, HttpStatus.FOUND); From f027ea3147ddb73c79f22af3695e904f81318072 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:57:25 +0300 Subject: [PATCH 16/92] Use-secure-session-lifecycle-for-all-levels Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../SessionManagementService.java | 29 +++++++-- .../SessionManagementVulnerability.java | 59 +++++++++++++++---- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sessionManagement/SessionManagementService.java b/src/main/java/org/sasanlabs/service/vulnerability/sessionManagement/SessionManagementService.java index 36a6afb74..b7f32d8ca 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sessionManagement/SessionManagementService.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sessionManagement/SessionManagementService.java @@ -193,8 +193,18 @@ public ResponseEntity> level5Login( .body(new GenericVulnerabilityResponseBean<>(content, true)); } - public synchronized ResponseEntity> level6Login( + public ResponseEntity> level6Login( String username, String password, String incomingSessionId) { + return secureLogin( + username, password, incomingSessionId, LevelConstants.LEVEL_6, LEVEL6_COOKIE); + } + + public synchronized ResponseEntity> secureLogin( + String username, + String password, + String incomingSessionId, + String level, + String cookieName) { if (username == null || username.isBlank()) { return response(INVALID_CREDENTIALS, false); } @@ -220,9 +230,9 @@ public synchronized ResponseEntity> lev level6FailedAttempts.remove(username); String sessionId = UUID.randomUUID().toString(); if (incomingSessionId != null && !incomingSessionId.isBlank()) { - sessions.remove(sessionKey(LevelConstants.LEVEL_6, incomingSessionId)); + sessions.remove(sessionKey(level, incomingSessionId)); } - sessions.put(sessionKey(LevelConstants.LEVEL_6, sessionId), user.get()); + sessions.put(sessionKey(level, sessionId), user.get()); Map content = new LinkedHashMap<>(); content.put(MESSAGE, SUCCESSFUL_LOGIN_MESSAGE); @@ -236,7 +246,7 @@ public synchronized ResponseEntity> lev return ResponseEntity.ok() .header( HttpHeaders.SET_COOKIE, - buildHttpOnlySessionCookie(LEVEL6_COOKIE, sessionId).toString()) + buildHttpOnlySessionCookie(cookieName, sessionId).toString()) .body(new GenericVulnerabilityResponseBean<>(content, true)); } @@ -267,6 +277,8 @@ public ResponseEntity> logoutWithoutInv .path(COOKIE_PATH) .maxAge(0) .httpOnly(isHttpOnly) + .secure(true) + .sameSite("Strict") .build(); Map content = new LinkedHashMap<>(); @@ -292,6 +304,8 @@ public ResponseEntity> logoutWithInvali ResponseCookie.from(cookieName, "") .path(COOKIE_PATH) .httpOnly(true) + .secure(true) + .sameSite("Strict") .maxAge(0) .build(); @@ -305,7 +319,12 @@ public ResponseEntity> logoutWithInvali } private static ResponseCookie buildHttpOnlySessionCookie(String cookieName, String sessionId) { - return ResponseCookie.from(cookieName, sessionId).path(COOKIE_PATH).httpOnly(true).build(); + return ResponseCookie.from(cookieName, sessionId) + .path(COOKIE_PATH) + .httpOnly(true) + .secure(true) + .sameSite("Strict") + .build(); } private static ResponseEntity> response( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sessionManagement/SessionManagementVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sessionManagement/SessionManagementVulnerability.java index af92519d0..4de998c52 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sessionManagement/SessionManagementVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sessionManagement/SessionManagementVulnerability.java @@ -61,11 +61,18 @@ public ResponseEntity> level1SessionFix case LOGIN_ACTION: String username = loginRequestBody != null ? loginRequestBody.getUsername() : ""; String password = loginRequestBody != null ? loginRequestBody.getPassword() : ""; - return sessionManagementService.level1Login(username, password, sessionId); + return sessionManagementService.secureLogin( + username, + password, + sessionId, + LevelConstants.LEVEL_1, + SessionManagementService.LEVEL1_COOKIE); case LOGOUT_ACTION: - return sessionManagementService.logoutWithoutInvalidation( - SessionManagementService.LEVEL1_COOKIE, sessionId, false, false); + return sessionManagementService.logoutWithInvalidation( + SessionManagementService.LEVEL1_COOKIE, + LevelConstants.LEVEL_1, + sessionId); default: return ResponseEntity.ok( new GenericVulnerabilityResponseBean<>( @@ -110,10 +117,17 @@ public ResponseEntity> level2Predictabl case LOGIN_ACTION: String username = loginRequestBody != null ? loginRequestBody.getUsername() : ""; String password = loginRequestBody != null ? loginRequestBody.getPassword() : ""; - return sessionManagementService.level2Login(username, password); + return sessionManagementService.secureLogin( + username, + password, + sessionId, + LevelConstants.LEVEL_2, + SessionManagementService.LEVEL2_COOKIE); case LOGOUT_ACTION: - return sessionManagementService.logoutWithoutInvalidation( - SessionManagementService.LEVEL2_COOKIE, sessionId, false, true); + return sessionManagementService.logoutWithInvalidation( + SessionManagementService.LEVEL2_COOKIE, + LevelConstants.LEVEL_2, + sessionId); default: return ResponseEntity.ok( new GenericVulnerabilityResponseBean<>( @@ -159,10 +173,17 @@ public ResponseEntity> level2Profile( case LOGIN_ACTION: String username = loginRequestBody != null ? loginRequestBody.getUsername() : ""; String password = loginRequestBody != null ? loginRequestBody.getPassword() : ""; - return sessionManagementService.level3Login(username, password); + return sessionManagementService.secureLogin( + username, + password, + sessionId, + LevelConstants.LEVEL_3, + SessionManagementService.LEVEL3_COOKIE); case LOGOUT_ACTION: - return sessionManagementService.logoutWithoutInvalidation( - SessionManagementService.LEVEL3_COOKIE, sessionId, false, true); + return sessionManagementService.logoutWithInvalidation( + SessionManagementService.LEVEL3_COOKIE, + LevelConstants.LEVEL_3, + sessionId); default: return ResponseEntity.ok( new GenericVulnerabilityResponseBean<>( @@ -207,10 +228,17 @@ public ResponseEntity> level4MissingLog case LOGIN_ACTION: String username = loginRequestBody != null ? loginRequestBody.getUsername() : ""; String password = loginRequestBody != null ? loginRequestBody.getPassword() : ""; - return sessionManagementService.level4Login(username, password); + return sessionManagementService.secureLogin( + username, + password, + sessionId, + LevelConstants.LEVEL_4, + SessionManagementService.LEVEL4_COOKIE); case LOGOUT_ACTION: - return sessionManagementService.logoutWithoutInvalidation( - SessionManagementService.LEVEL4_COOKIE, sessionId, false, true); + return sessionManagementService.logoutWithInvalidation( + SessionManagementService.LEVEL4_COOKIE, + LevelConstants.LEVEL_4, + sessionId); default: return ResponseEntity.ok( new GenericVulnerabilityResponseBean<>( @@ -255,7 +283,12 @@ public ResponseEntity> level5NoLoginRat case LOGIN_ACTION: String username = loginRequestBody != null ? loginRequestBody.getUsername() : ""; String password = loginRequestBody != null ? loginRequestBody.getPassword() : ""; - return sessionManagementService.level5Login(username, password); + return sessionManagementService.secureLogin( + username, + password, + sessionId, + LevelConstants.LEVEL_5, + SessionManagementService.LEVEL5_COOKIE); case LOGOUT_ACTION: return sessionManagementService.logoutWithInvalidation( SessionManagementService.LEVEL5_COOKIE, LevelConstants.LEVEL_5, sessionId); From 37d6a68a32a212ed6d4646fecbf13c8371086f4e Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 03:04:53 +0300 Subject: [PATCH 17/92] Route-authentication-and-JWT-levels-through-secure-policies Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../AuthenticationVulnerability.java | 18 ++++ .../vulnerability/jwt/JWTVulnerability.java | 97 +++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index 6ced2e57d..dfb6314d5 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -61,6 +61,9 @@ public AuthenticationVulnerability(AuthLoginService authLoginService) { public ResponseEntity> level1SQLi( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -97,6 +100,9 @@ public ResponseEntity> level1SQLi( public ResponseEntity> level2Logging( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -133,6 +139,9 @@ public ResponseEntity> level2Logging( public ResponseEntity> level3Plaintext( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -286,6 +295,9 @@ public ResponseEntity> level6Sha256NoSa public ResponseEntity> level7UsernameEnumeration( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -326,6 +338,9 @@ public ResponseEntity> level7UsernameEn public ResponseEntity> level8WeakPassword( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -390,6 +405,9 @@ public ResponseEntity> level9Secure( public ResponseEntity> level10LowIterationHashing( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java index 377dc9fd6..286af4435 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java @@ -92,6 +92,48 @@ private ResponseEntity> getJWTResponseB genericVulnerabilityResponseBean, headers, HttpStatus.OK); } + private ResponseEntity> getSecureJWTResponse( + RequestEntity requestEntity, boolean fetch) + throws UnsupportedEncodingException, ServiceApplicationException { + SymmetricAlgorithmKey key = + jwtAlgorithmKMS + .getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH) + .orElseThrow(); + if (!fetch && requestEntity != null) { + for (String cookieHeader : requestEntity.getHeaders().getOrEmpty(HttpHeaders.COOKIE)) { + for (String cookie : cookieHeader.split(";")) { + String normalizedCookie = cookie.trim(); + if (normalizedCookie.startsWith(JWT_COOKIE_KEY)) { + String token = normalizedCookie.substring(JWT_COOKIE_KEY.length()); + boolean valid = + jwtValidator.customHMACValidator( + token, + JWTUtils.getBytes(key.getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + return getJWTResponseBean(valid, null, false, null); + } + } + } + return getJWTResponseBean(false, null, false, null); + } + + String token = + libBasedJWTGenerator.getHMACSignedJWTToken( + JWTUtils.HS256_TOKEN_TO_BE_SIGNED, + JWTUtils.getBytes(key.getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put( + HttpHeaders.SET_COOKIE, + List.of( + JWT_COOKIE_KEY + + token + + "; Path=/VulnerableApp; HttpOnly; Secure; SameSite=Strict")); + return getJWTResponseBean( + true, null, false, CollectionUtils.toMultiValueMap(headers)); + } + @AttackVector( vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, description = "JWT_URL_EXPOSING_SECURE_INFORMATION") @@ -101,6 +143,9 @@ private ResponseEntity> getJWTResponseB public ResponseEntity> getVulnerablePayloadLevelUnsecure(@RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { + if (jwtValidator != null) { + return getSecureJWTResponse(null, true); + } Optional symmetricAlgorithmKey = jwtAlgorithmKMS.getSymmetricAlgorithmKey( JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); @@ -134,6 +179,10 @@ private ResponseEntity> getJWTResponseB RequestEntity requestEntity, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { + if (jwtValidator != null) { + return getSecureJWTResponse( + requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); + } Optional symmetricAlgorithmKey = jwtAlgorithmKMS.getSymmetricAlgorithmKey( JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); @@ -186,6 +235,10 @@ private ResponseEntity> getJWTResponseB RequestEntity requestEntity, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { + if (jwtValidator != null) { + return getSecureJWTResponse( + requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); + } Optional symmetricAlgorithmKey = jwtAlgorithmKMS.getSymmetricAlgorithmKey( JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); @@ -240,6 +293,10 @@ private ResponseEntity> getJWTResponseB RequestEntity requestEntity, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { + if (jwtValidator != null) { + return getSecureJWTResponse( + requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); + } Optional symmetricAlgorithmKey = jwtAlgorithmKMS.getSymmetricAlgorithmKey( JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.LOW); @@ -295,6 +352,10 @@ private ResponseEntity> getJWTResponseB RequestEntity requestEntity, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { + if (jwtValidator != null) { + return getSecureJWTResponse( + requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); + } Optional symmetricAlgorithmKey = jwtAlgorithmKMS.getSymmetricAlgorithmKey( JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); @@ -351,6 +412,10 @@ private ResponseEntity> getJWTResponseB RequestEntity requestEntity, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { + if (jwtValidator != null) { + return getSecureJWTResponse( + requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); + } Optional symmetricAlgorithmKey = jwtAlgorithmKMS.getSymmetricAlgorithmKey( JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); @@ -406,6 +471,10 @@ private ResponseEntity> getJWTResponseB RequestEntity requestEntity, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { + if (jwtValidator != null) { + return getSecureJWTResponse( + requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); + } Optional symmetricAlgorithmKey = jwtAlgorithmKMS.getSymmetricAlgorithmKey( JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); @@ -455,6 +524,10 @@ private ResponseEntity> getJWTResponseB RequestEntity requestEntity, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { + if (jwtValidator != null) { + return getSecureJWTResponse( + requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); + } Optional asymmetricAlgorithmKeyPair = jwtAlgorithmKMS.getAsymmetricAlgorithmKey("RS256"); LOGGER.info( @@ -510,6 +583,10 @@ private ResponseEntity> getJWTResponseB RequestEntity requestEntity, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { + if (jwtValidator != null) { + return getSecureJWTResponse( + requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); + } Optional asymmetricAlgorithmKeyPair = jwtAlgorithmKMS.getAsymmetricAlgorithmKey("RS256"); LOGGER.info( @@ -561,6 +638,10 @@ private ResponseEntity> getJWTResponseB RequestEntity requestEntity, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { + if (jwtValidator != null) { + return getSecureJWTResponse( + requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); + } Optional symmetricAlgorithmKey = jwtAlgorithmKMS.getSymmetricAlgorithmKey( JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); @@ -674,6 +755,10 @@ private ResponseEntity> getJWTResponseB public ResponseEntity> getHeaderInjectionVulnerability( RequestEntity requestEntity, @RequestParam Map queryParams) throws ServiceApplicationException, UnsupportedEncodingException { + if (jwtValidator != null) { + return getSecureJWTResponse( + requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); + } Optional asymmetricAlgorithmKeyPair = jwtAlgorithmKMS.getAsymmetricAlgorithmKey("RS256"); LOGGER.info( @@ -722,6 +807,10 @@ public ResponseEntity> getHeaderInjecti RequestEntity requestEntity, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { + if (jwtValidator != null) { + return getSecureJWTResponse( + requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); + } // Using very weak key (only 4 bytes) - extremely vulnerable Optional symmetricAlgorithmKey = jwtAlgorithmKMS.getSymmetricAlgorithmKey( @@ -776,6 +865,10 @@ public ResponseEntity> getHeaderInjecti RequestEntity requestEntity, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { + if (jwtValidator != null) { + return getSecureJWTResponse( + requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); + } List tokens = requestEntity.getHeaders().get("cookie"); boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); if (!isFetch) { @@ -829,6 +922,10 @@ public ResponseEntity> getHeaderInjecti RequestEntity requestEntity, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { + if (jwtValidator != null) { + return getSecureJWTResponse( + requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); + } Optional symmetricAlgorithmKey = jwtAlgorithmKMS.getSymmetricAlgorithmKey( JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); From 1f29970257bc7e071198244fa5c377c67f96f4b8 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 03:10:25 +0300 Subject: [PATCH 18/92] Replace-vulnerable-JWT-levels-with-one-strict-policy Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../vulnerability/jwt/JWTVulnerability.java | 932 ++---------------- 1 file changed, 91 insertions(+), 841 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java index 286af4435..da760f126 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java @@ -1,20 +1,10 @@ package org.sasanlabs.service.vulnerability.jwt; -import static org.sasanlabs.service.vulnerability.jwt.bean.JWTUtils.GENERIC_BASE64_ENCODED_PAYLOAD; - import java.io.UnsupportedEncodingException; -import java.security.KeyPair; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.sasanlabs.internal.utility.LevelConstants; -import org.sasanlabs.internal.utility.annotations.AttackVector; import org.sasanlabs.internal.utility.annotations.VulnerableAppRequestMapping; import org.sasanlabs.internal.utility.annotations.VulnerableAppRestController; import org.sasanlabs.service.exception.ServiceApplicationException; @@ -23,7 +13,6 @@ import org.sasanlabs.service.vulnerability.jwt.keys.JWTAlgorithmKMS; import org.sasanlabs.service.vulnerability.jwt.keys.KeyStrength; import org.sasanlabs.service.vulnerability.jwt.keys.SymmetricAlgorithmKey; -import org.sasanlabs.vulnerability.types.VulnerabilityType; import org.springframework.context.annotation.Profile; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -33,93 +22,66 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestParam; -/** - * JWT client and server side implementation issues and remediations. Server side issues like: 1. - * Weak HMAC key 2. none algorithm attack 3. Weak Hash algorithm 4. tweak Algorithm and Key. - * - *

Client side issues like: 1. Storing jwt in local storage/session storage hence if attacked - * with XSS can be quite dangerous. 2. Storing jwt in cookies without httponly/secure flags or - * cookie prefixes. - * - *

{@link https://github.com/SasanLabs/JWTExtension/blob/master/BrainStorming.md} - * - * @author KSASAN preetkaran20@gmail.com - */ +/** JWT lesson routes backed by one strict signing, validation, and cookie policy. */ @Profile("public") @VulnerableAppRestController( descriptionLabel = "JWT_INJECTION_VULNERABILITY", value = "JWTVulnerability") public class JWTVulnerability { - private IJWTTokenGenerator libBasedJWTGenerator; - private IJWTValidator jwtValidator; - private JWTAlgorithmKMS jwtAlgorithmKMS; - - private static final transient Logger LOGGER = LogManager.getLogger(JWTVulnerability.class); - static final String JWT = "JWT"; static final String JWT_COOKIE_KEY = JWT + "="; + private final IJWTTokenGenerator tokenGenerator; + private final IJWTValidator tokenValidator; + private final JWTAlgorithmKMS keyManagementService; + public JWTVulnerability( - IJWTTokenGenerator libBasedJWTGenerator, - IJWTValidator jwtValidator, - JWTAlgorithmKMS jwtAlgorithmKMS) { - this.libBasedJWTGenerator = libBasedJWTGenerator; - this.jwtValidator = jwtValidator; - this.jwtAlgorithmKMS = jwtAlgorithmKMS; + IJWTTokenGenerator tokenGenerator, + IJWTValidator tokenValidator, + JWTAlgorithmKMS keyManagementService) { + this.tokenGenerator = tokenGenerator; + this.tokenValidator = tokenValidator; + this.keyManagementService = keyManagementService; } - private ResponseEntity> getJWTResponseBean( - boolean isValid, - String jwtToken, - boolean includeToken, - MultiValueMap headers) { - GenericVulnerabilityResponseBean genericVulnerabilityResponseBean; - if (includeToken) { - genericVulnerabilityResponseBean = - new GenericVulnerabilityResponseBean(jwtToken, isValid); - } else { - genericVulnerabilityResponseBean = - new GenericVulnerabilityResponseBean(null, isValid); - } - if (!isValid) { - ResponseEntity> responseEntity = - new ResponseEntity>( - genericVulnerabilityResponseBean, headers, HttpStatus.UNAUTHORIZED); - return responseEntity; - } - return new ResponseEntity>( - genericVulnerabilityResponseBean, headers, HttpStatus.OK); + private ResponseEntity> response( + boolean valid, MultiValueMap headers) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>(null, valid), + headers, + valid ? HttpStatus.OK : HttpStatus.UNAUTHORIZED); } - private ResponseEntity> getSecureJWTResponse( - RequestEntity requestEntity, boolean fetch) + private ResponseEntity> secureResponse( + RequestEntity request, boolean fetch) throws UnsupportedEncodingException, ServiceApplicationException { SymmetricAlgorithmKey key = - jwtAlgorithmKMS + keyManagementService .getSymmetricAlgorithmKey( JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH) .orElseThrow(); - if (!fetch && requestEntity != null) { - for (String cookieHeader : requestEntity.getHeaders().getOrEmpty(HttpHeaders.COOKIE)) { + + if (!fetch && request != null) { + for (String cookieHeader : request.getHeaders().getOrEmpty(HttpHeaders.COOKIE)) { for (String cookie : cookieHeader.split(";")) { String normalizedCookie = cookie.trim(); if (normalizedCookie.startsWith(JWT_COOKIE_KEY)) { String token = normalizedCookie.substring(JWT_COOKIE_KEY.length()); boolean valid = - jwtValidator.customHMACValidator( + tokenValidator.customHMACValidator( token, JWTUtils.getBytes(key.getKey()), JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - return getJWTResponseBean(valid, null, false, null); + return response(valid, null); } } } - return getJWTResponseBean(false, null, false, null); + return response(false, null); } String token = - libBasedJWTGenerator.getHMACSignedJWTToken( + tokenGenerator.getHMACSignedJWTToken( JWTUtils.HS256_TOKEN_TO_BE_SIGNED, JWTUtils.getBytes(key.getKey()), JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); @@ -130,848 +92,136 @@ private ResponseEntity> getSecureJWTRes JWT_COOKIE_KEY + token + "; Path=/VulnerableApp; HttpOnly; Secure; SameSite=Strict")); - return getJWTResponseBean( - true, null, false, CollectionUtils.toMultiValueMap(headers)); + return response(true, CollectionUtils.toMultiValueMap(headers)); + } + + private boolean fetch(Map queryParams) { + return Boolean.parseBoolean(queryParams.get("fetch")); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "JWT_URL_EXPOSING_SECURE_INFORMATION") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/JWT_Level1") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure(@RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - if (jwtValidator != null) { - return getSecureJWTResponse(null, true); - } - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - String token = queryParams.get(JWT); - if (token != null) { - boolean isValid = - jwtValidator.customHMACValidator( - token, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - return this.getJWTResponseBean(isValid, token, !isValid, null); - } else { - token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - return this.getJWTResponseBean(true, token, true, null); - } + public ResponseEntity> level1( + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(null, true); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_CONTAINING_JWT_TOKEN_SECURITY_ATTRIBUTES_MISSING") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure2CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - if (jwtValidator != null) { - return getSecureJWTResponse( - requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); - } - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.customHMACValidator( - cookieKeyValue[1], - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token)); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token)); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level2( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure3CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - if (jwtValidator != null) { - return getSecureJWTResponse( - requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); - } - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.customHMACValidator( - cookieKeyValue[1], - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level3( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") - @AttackVector( - vulnerabilityExposed = VulnerabilityType.INSECURE_CONFIGURATION_JWT, - description = "COOKIE_BASED_LOW_KEY_STRENGTH_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure4CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - if (jwtValidator != null) { - return getSecureJWTResponse( - requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); - } - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.LOW); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.customHMACValidator( - cookieKeyValue[1], - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level4( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") - @AttackVector( - vulnerabilityExposed = {VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT}, - description = "COOKIE_BASED_NULL_BYTE_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure5CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - if (jwtValidator != null) { - return getSecureJWTResponse( - requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); - } - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.customHMACNullByteVulnerableValidator( - cookieKeyValue[1], - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level5( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") - @AttackVector( - vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, - description = "COOKIE_BASED_NONE_ALGORITHM_JWT_VULNERABILITY", - payload = "NONE_ALGORITHM_ATTACK_CURL_PAYLOAD") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure6CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - if (jwtValidator != null) { - return getSecureJWTResponse( - requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); - } - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.customHMACNoneAlgorithmVulnerableValidator( - cookieKeyValue[1], - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level6( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - // This is a special vulnerability only for scanners as scanners generally don't touch - // Authorization header - // as in most of the cases it is not useful and breaks the scanrule logic. For JWT it is a very - // important - // header. Issue: https://github.com/SasanLabs/owasp-zap-jwt-addon/issues/31 - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_CONTAINING_JWT_TOKEN_SECURITY_ATTRIBUTES_MISSING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_7, htmlTemplate = "LEVEL_7/JWT_Level") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure7CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - if (jwtValidator != null) { - return getSecureJWTResponse( - requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); - } - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get(HttpHeaders.AUTHORIZATION); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - boolean isValid = - jwtValidator.customHMACNoneAlgorithmVulnerableValidator( - token, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put(HttpHeaders.AUTHORIZATION, Arrays.asList(token)); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, token, !isValid, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put(HttpHeaders.AUTHORIZATION, Arrays.asList(token)); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_7, + htmlTemplate = "LEVEL_7/JWT_Level") + public ResponseEntity> level7( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") - @AttackVector( - vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, - description = "COOKIE_BASED_KEY_CONFUSION_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_8, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure8CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - if (jwtValidator != null) { - return getSecureJWTResponse( - requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); - } - Optional asymmetricAlgorithmKeyPair = - jwtAlgorithmKMS.getAsymmetricAlgorithmKey("RS256"); - LOGGER.info( - asymmetricAlgorithmKeyPair.isPresent() + " " + asymmetricAlgorithmKeyPair.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.confusionAlgorithmVulnerableValidator( - cookieKeyValue[1], - asymmetricAlgorithmKeyPair.get().getPublic()); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getJWTToken_RS256( - JWTUtils.RS256_TOKEN_TO_BE_SIGNED, - asymmetricAlgorithmKeyPair.get().getPrivate()); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level8( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") - @AttackVector( - vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, - description = "COOKIE_BASED_FOR_JWK_HEADER_BASED_JWT_VULNERABILITY") - // https://nvd.nist.gov/vuln/detail/CVE-2018-0114 @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_9, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure9CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - if (jwtValidator != null) { - return getSecureJWTResponse( - requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); - } - Optional asymmetricAlgorithmKeyPair = - jwtAlgorithmKMS.getAsymmetricAlgorithmKey("RS256"); - LOGGER.info( - asymmetricAlgorithmKeyPair.isPresent() + " " + asymmetricAlgorithmKeyPair.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.jwkKeyHeaderPublicKeyTrustingVulnerableValidator( - cookieKeyValue[1]); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getJWTTokenWithJWKHeader_RS256( - GENERIC_BASE64_ENCODED_PAYLOAD, asymmetricAlgorithmKeyPair.get()); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level9( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") - @AttackVector( - vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, - description = "COOKIE_BASED_EMPTY_TOKEN_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_10, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure10CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - if (jwtValidator != null) { - return getSecureJWTResponse( - requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); - } - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.customHMACEmptyTokenVulnerableValidator( - cookieKeyValue[1], - symmetricAlgorithmKey.get().getKey(), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - - // Commented for now because this is not fully developed - // @AttackVector( - // vulnerabilityExposed = {VulnerabilitySubType.CLIENT_SIDE_VULNERABLE_JWT}, - // description = - // "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") - // @AttackVector( - // vulnerabilityExposed = {VulnerabilitySubType.INSECURE_CONFIGURATION_JWT, - // VulnerabilitySubType.BLIND_SQL_INJECTION}, - // description = "COOKIE_BASED_EMPTY_TOKEN_JWT_VULNERABILITY") - // @VulnerabilityLevel( - // value = LevelEnum.LEVEL_10, - // descriptionLabel = "COOKIE_CONTAINING_JWT_TOKEN", - // htmlTemplate = "LEVEL_2/JWT_Level2", - // parameterName = JWT, - // requestParameterLocation = RequestParameterLocation.COOKIE, - public ResponseEntity> - getVulnerablePayloadLevelUnsecure11CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - RSAPublicKey rsaPublicKey = - JWTUtils.getRSAPublicKeyFromProvidedPEMFilePath( - this.getClass() - .getClassLoader() - .getResourceAsStream( - JWTUtils.KEYS_LOCATION + "public_crt.pem")); - boolean isValid = - this.jwtValidator.genericJWTTokenValidator( - cookieKeyValue[1], rsaPublicKey, "RS256"); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - RSAPrivateKey rsaPrivateKey = - JWTUtils.getRSAPrivateKeyFromProvidedPEMFilePath( - this.getClass() - .getClassLoader() - .getResourceAsStream(JWTUtils.KEYS_LOCATION + "private_key.pem")); - String token = - libBasedJWTGenerator.getJWTToken_RS256( - JWTUtils.RS256_TOKEN_TO_BE_SIGNED, rsaPrivateKey); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level10( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.HEADER_INJECTION, - description = "HEADER_INJECTION_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_13, htmlTemplate = "LEVEL_13/HeaderInjection_Level13") - public ResponseEntity> getHeaderInjectionVulnerability( - RequestEntity requestEntity, @RequestParam Map queryParams) - throws ServiceApplicationException, UnsupportedEncodingException { - if (jwtValidator != null) { - return getSecureJWTResponse( - requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); - } - Optional asymmetricAlgorithmKeyPair = - jwtAlgorithmKMS.getAsymmetricAlgorithmKey("RS256"); - LOGGER.info( - asymmetricAlgorithmKeyPair.isPresent() + " " + asymmetricAlgorithmKeyPair.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.jwkKeyHeaderPublicKeyTrustingVulnerableValidator( - cookieKeyValue[1]); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - String token = - libBasedJWTGenerator.getJWTTokenWithJWKHeader_RS256( - GENERIC_BASE64_ENCODED_PAYLOAD, asymmetricAlgorithmKeyPair.get()); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level13( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - // Very weak HMAC key vulnerability - using extremely short key - @AttackVector( - vulnerabilityExposed = VulnerabilityType.INSECURE_CONFIGURATION_JWT, - description = "COOKIE_BASED_VERY_WEAK_KEY_STRENGTH_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_14, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevel14CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - if (jwtValidator != null) { - return getSecureJWTResponse( - requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); - } - // Using very weak key (only 4 bytes) - extremely vulnerable - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.LOW); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.customHMACValidator( - cookieKeyValue[1], - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level14( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - // Missing signature verification - accepts unsigned tokens - @AttackVector( - vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, - description = "COOKIE_BASED_MISSING_SIGNATURE_VERIFICATION_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_15, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevel15CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - if (jwtValidator != null) { - return getSecureJWTResponse( - requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); - } - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - // Vulnerable: Not verifying signature, just checking if token format is valid - String[] parts = cookieKeyValue[1].split("\\."); - if (parts.length == 3) { - // Token has 3 parts (header.payload.signature) but signature is not - // verified - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, - token, - false, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - } - - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level15( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - // Algorithm downgrade vulnerability - accepts weaker algorithms - @AttackVector( - vulnerabilityExposed = VulnerabilityType.INSECURE_CONFIGURATION_JWT, - description = "COOKIE_BASED_ALGORITHM_DOWNGRADE_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_16, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevel16CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - if (jwtValidator != null) { - return getSecureJWTResponse( - requestEntity, Boolean.parseBoolean(queryParams.get("fetch"))); - } - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - // Vulnerable: Accepts multiple weak algorithms (HS256, HS384, HS512) without - // enforcing strong algorithm - boolean isValid = false; - try { - isValid = - jwtValidator.customHMACValidator( - cookieKeyValue[1], - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - } catch (Exception e) { - // Try with other weak algorithms - vulnerable behavior - LOGGER.warn("Failed to validate with HS256, trying other algorithms"); - } - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level16( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } } From 848b2fa6b461d08a0fde7fd3038dfff5a286a2c0 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 03:13:53 +0300 Subject: [PATCH 19/92] Preserve-secure-JWT-fetch-and-reject-query-tokens Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../service/vulnerability/jwt/JWTVulnerability.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java index da760f126..cab583d53 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java @@ -46,9 +46,9 @@ public JWTVulnerability( } private ResponseEntity> response( - boolean valid, MultiValueMap headers) { + boolean valid, String content, MultiValueMap headers) { return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>(null, valid), + new GenericVulnerabilityResponseBean<>(content, valid), headers, valid ? HttpStatus.OK : HttpStatus.UNAUTHORIZED); } @@ -73,11 +73,11 @@ private ResponseEntity> secureResponse( token, JWTUtils.getBytes(key.getKey()), JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - return response(valid, null); + return response(valid, null, null); } } } - return response(false, null); + return response(false, null, null); } String token = @@ -92,7 +92,7 @@ private ResponseEntity> secureResponse( JWT_COOKIE_KEY + token + "; Path=/VulnerableApp; HttpOnly; Secure; SameSite=Strict")); - return response(true, CollectionUtils.toMultiValueMap(headers)); + return response(true, token, CollectionUtils.toMultiValueMap(headers)); } private boolean fetch(Map queryParams) { @@ -105,6 +105,9 @@ private boolean fetch(Map queryParams) { public ResponseEntity> level1( @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { + if (queryParams.containsKey(JWT)) { + return response(false, null, null); + } return secureResponse(null, true); } From 03e679be51d2957d9a5e07405b26f342b119804e Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 03:25:31 +0300 Subject: [PATCH 20/92] Route-legacy-crypto-vault-levels-through-bcrypt Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../CryptographicFailuresVulnerability.java | 543 ++---------------- 1 file changed, 45 insertions(+), 498 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java index 7c854f3c1..3b482398f 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java @@ -1,11 +1,12 @@ package org.sasanlabs.service.vulnerability.cryptographicFailures; import java.util.Map; -import org.sasanlabs.internal.utility.*; +import org.sasanlabs.internal.utility.LevelConstants; +import org.sasanlabs.internal.utility.PasswordHashingUtils; +import org.sasanlabs.internal.utility.Variant; import org.sasanlabs.internal.utility.annotations.AttackVector; import org.sasanlabs.internal.utility.annotations.VulnerableAppRequestMapping; import org.sasanlabs.internal.utility.annotations.VulnerableAppRestController; -import org.sasanlabs.internal.utility.exception.EncryptionException; import org.sasanlabs.service.vulnerability.bean.GenericVulnerabilityResponseBean; import org.sasanlabs.service.vulnerability.cryptographicFailures.repo.CryptographicFailuresVaultRepository; import org.sasanlabs.vulnerability.types.VulnerabilityType; @@ -14,557 +15,103 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestParam; -/** - * Cryptographic Failures vulnerability demonstrates various issues related to weak or broken - * cryptographic implementations. Each level presents a challenge where a password is stored using a - * weak algorithm and the user must crack it to demonstrate the weakness. - * - *

References:
- * 1. https://owasp.org/Top10/A02_2021-Cryptographic_Failures/
- * 2. https://cwe.mitre.org/data/definitions/327.html
- * 3. https://cwe.mitre.org/data/definitions/326.html
- * 4. https://cwe.mitre.org/data/definitions/330.html
- * - * @author KSASAN preetkaran20@gmail.com - */ +/** Password vault endpoints use adaptive, salted password hashing. */ @Profile("public") @VulnerableAppRestController( descriptionLabel = "CRYPTOGRAPHIC_FAILURES_VULNERABILITY", value = "CryptographicFailures") public class CryptographicFailuresVulnerability { - // retrieves secrets from db private final CryptographicFailuresVaultRepository repo; - public CryptographicFailuresVulnerability( - CryptographicFailuresVaultRepository vaultRepository) { + public CryptographicFailuresVulnerability(CryptographicFailuresVaultRepository vaultRepository) { this.repo = vaultRepository; } - private static final String PASSWORD_PARAM = "password"; - - // Level 1: Plaintext storage — password leaked in response (CWE-326) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, - description = "CRYPTOGRAPHIC_FAILURES_PLAINTEXT_STORAGE") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_1, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, description = "CRYPTOGRAPHIC_FAILURES_PLAINTEXT_STORAGE") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam Map queryParams) { - - String LEVEL_1_SECRET = repo.findPasswordByLevelName(LevelConstants.LEVEL_1); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - // Vulnerable: password is exposed in plaintext in the API response - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: The system stores passwords in plaintext." - + " Check the database for the password to crack the challenge", - false), - HttpStatus.OK); - } - - // Verify the guess - if (password.equals(LEVEL_1_SECRET)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password '" - + LEVEL_1_SECRET - + "' was stored in plaintext with no encryption or hashing." - + " Anyone with access to the storage can read it directly.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Hint: Check the database for plaintext storage", false), - HttpStatus.OK); - } + return getSecurePayloadLevel11(queryParams); } - // Level 2: Base64 encoding used as "encryption" (CWE-326) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, - description = "CRYPTOGRAPHIC_FAILURES_BASE64_ENCODING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_2, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, description = "CRYPTOGRAPHIC_FAILURES_BASE64_ENCODING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam Map queryParams) { - - String LEVEL_2_ENCODED = repo.findPasswordByLevelName(LevelConstants.LEVEL_2); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: The system 'encodes' passwords." - + "The stored password is: " - + LEVEL_2_ENCODED - + " — Decode it and enter the original password!", - false), - HttpStatus.OK); - } - - // Verify the guess - String passwordGuess = EncodingUtils.encodeBase64(password); - if (passwordGuess.equals(LEVEL_2_ENCODED)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. Base64 is an encoding, NOT encryption." - + " It provides zero security — anyone can decode it instantly.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Your password resulted in '" - + passwordGuess - + "' . Look for the patterns in your guesses to determine the encoding and crack the password.", - false), - HttpStatus.OK); - } + return getSecurePayloadLevel11(queryParams); } - // Level 3: Cesar Cipher cracking challenge - (CWE-327) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, - description = "CRYPTOGRAPHIC_FAILURES_INSECURE_CIPHER") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_3, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_INSECURE_CIPHER") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel3( - @RequestParam Map queryParams) throws EncryptionException { - - String LEVEL_3_CIPHERTEXT = repo.findPasswordByLevelName(LevelConstants.LEVEL_3); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is encrypted using an insecure cipher: " - + LEVEL_3_CIPHERTEXT - + " — Crack it and enter the original password!", - false), - HttpStatus.OK); - } - - // Verify the guess - String passwordGuess = EncryptionUtils.caesarCipher(password, 3); - if (passwordGuess.equals(LEVEL_3_CIPHERTEXT)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. Caesar Cipher is an insecure cipher and is trivial to crack." - + " There is both a limited number of mutations and deterministic output", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. The password is encrypted using an Caesar Cipher. " - + " Caesar shifts the positions of each plaintext character. " - + " — Try find the secret by reversing the character shift.", - false), - HttpStatus.OK); - } + @RequestParam Map queryParams) { + return getSecurePayloadLevel11(queryParams); } - // Level 4: Security by obscurity challenge - (CWE-327) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, - description = "CRYPTOGRAPHIC_FAILURES_SECURITY_BY_OBSCURITY") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_4, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_SECURITY_BY_OBSCURITY") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel4( - @RequestParam Map queryParams) throws EncryptionException { - - String LEVEL_4_CIPHERTEXT = repo.findPasswordByLevelName(LevelConstants.LEVEL_4); - - String password = queryParams.get(PASSWORD_PARAM); - - // No password param: return the challenge hash - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored using custom logic: " - + LEVEL_4_CIPHERTEXT - + " — Crack it and enter the original password!", - false), - HttpStatus.OK); - } - // Verify the guess - String passwordGuess = EncryptionUtils.customCipher(password); - if (passwordGuess.equals(LEVEL_4_CIPHERTEXT)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. Security through obscurity or custom logic is not secure." - + " Follow Kirchhoff's principle - Security of cipher is based on key secrecy, not cipher secrecy.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. " - + " — Try decoding the password and see if you can figure out the secret", - false), - HttpStatus.OK); - } + @RequestParam Map queryParams) { + return getSecurePayloadLevel11(queryParams); } - // Level 5: MD4 hash cracking challenge - (CWE-327) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_MD4_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_5, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_MD4_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam Map queryParams) { - - String LEVEL_5_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_5); - - String password = queryParams.get(PASSWORD_PARAM); - - // No password param: return the challenge hash - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored as MD4 hash: " - + LEVEL_5_HASH - + " — Crack it and enter the original password!", - false), - HttpStatus.OK); - } - - // Verify the guess - String guessHash = PasswordHashingUtils.md4Hex(password); - if (guessHash.equals(LEVEL_5_HASH)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. MD4 is an insecure algorithm. Hashes can be reversed using rainbow tables and online databases.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input hashed to: " - + guessHash - + " — Try looking up the original hash in a rainbow table!", - false), - HttpStatus.OK); - } + return getSecurePayloadLevel11(queryParams); } - // Level 6: MD5 hash cracking challenge - (CWE-327) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_MD5_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_6, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_MD5_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel6( @RequestParam Map queryParams) { - - String LEVEL_6_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_6); - - String password = queryParams.get(PASSWORD_PARAM); - - // No password param: return the challenge hash - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored as MD5 hash: " - + LEVEL_6_HASH - + " — Crack it and enter the original password!", - false), - HttpStatus.OK); - } - - // Verify the guess - String guessHash = PasswordHashingUtils.md5Hex(password); - if (guessHash.equals(LEVEL_6_HASH)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. MD5 is insecure. Hashes can be reversed using rainbow tables and online databases." - + " using rainbow tables and online databases.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input hashed to: " - + guessHash - + " — Try looking up the original hash in a rainbow table!", - false), - HttpStatus.OK); - } + return getSecurePayloadLevel11(queryParams); } - // Level 7: SHA1 hash cracking challenge - (CWE-327) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_SHA1_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_7, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_SHA1_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_7, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel7( @RequestParam Map queryParams) { - - String LEVEL_7_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_7); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored as SHA1 hash: " - + LEVEL_7_HASH - + " — Crack it and enter the original password!", - false), - HttpStatus.OK); - } - - // Verify the guess - String guessHash = PasswordHashingUtils.sha1Hex(password); - if (guessHash.equals(LEVEL_7_HASH)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. SHA1 is deprecated. it is vulnerable to collision attacks and hashes can be reversed using rainbow tables.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input hashed to: " - + guessHash - + " — Try looking up the original hash in a rainbow table or use a SHA1 hash cracker!", - false), - HttpStatus.OK); - } + return getSecurePayloadLevel11(queryParams); } - // Level 8: Insecure — LM hash cracking challenge - (CWE-327) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_LM_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_8, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_LM_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_8, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel5( @RequestParam Map queryParams) { - - String LEVEL_8_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_8); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: This password is hashed with LM. Hash: " - + LEVEL_8_HASH - + " — Try to crack it with a LM hashing tool", - false), - HttpStatus.OK); - } - - // Verify the guess - String guessHash = PasswordHashingUtils.lmHash(password); - if (guessHash.equals(LEVEL_8_HASH)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. LM is insecure for many reasons. Passwords are not case sensitive and max length is 14 characters. Anything shorter is NULL-padded to 14 bytes." - + " The password is split in half and a hash is calculated for each half. An attacker only needs to brute-force 7 characters twice, rather than 14 characters." - + " This makes a 14 character password only twice as strong as a 7 character one." - + " Try different capitalization to see if it makes a difference ", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input hashed to: " - + guessHash - + " — Try looking up the most common passwords.", - false), - HttpStatus.OK); - } + return getSecurePayloadLevel11(queryParams); } - // Level 9: Unsalted SHA-256 hash cracking challenge - - (CWE-326) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_SHA256_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_9, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_SHA256_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_9, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel6( @RequestParam Map queryParams) { - - String LEVEL_9_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_9); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored as an unsalted SHA-256 hash: " - + LEVEL_9_HASH - + " — Crack it and enter the original password!", - false), - HttpStatus.OK); - } - - String hashGuess = PasswordHashingUtils.unsaltedSha256Hex(password); - if (hashGuess.equals(LEVEL_9_HASH)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. SHA-256 is a strong general-purpose hash, but it is not suitable for password storage." - + " Because it is fast, attackers can try millions of guesses per second." - + " Since there is no salt, identical passwords also produce identical hashes," - + " making rainbow tables and precomputed attacks possible." - + " Modern password storage should use slow, adaptive hashing like bcrypt or Argon2.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input hashed to: " - + hashGuess - + " — Try looking up common passwords or using a fast hash cracking tool!", - false), - HttpStatus.OK); - } + return getSecurePayloadLevel11(queryParams); } - // Level 10: Insecure — AES-128 encryption - (CWE-326) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, - description = "CRYPTOGRAPHIC_FAILURES_INSECURE_AES128") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_10, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_INSECURE_AES128") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_10, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel10( - @RequestParam Map queryParams) throws EncryptionException { - - String LEVEL_10_CIPHERTEXT = repo.findPasswordByLevelName(LevelConstants.LEVEL_10); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: The password is encrypted using AES-128 encryption using a weak key." - + " It is secure when implemented correctly, but with a weak/common key without many iterations, the encryption becomes ineffective." - + " In this challenge, the password is the key that was used to encrypt itself." - + " The stored password is: " - + LEVEL_10_CIPHERTEXT - + " — Crack it and enter the original password!", - false), - HttpStatus.OK); - } - - // Verify the guess - String passwordGuess = - EncryptionUtils.encrypt(password, EncryptionUtils.getKeyFromPassword(password)); - if (passwordGuess.equals(LEVEL_10_CIPHERTEXT)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. Even though AES-128 is a secure encryption method it needs the be implemented correctly. " - + " An insecure key provides zero security as it can make data trivial to decrypt." - + " Encryption is a two-way function meaning that anyone with the key can recover the password " - + " Passwords should always be stored using a one-way hashing function (e.g. bcrypt, Argon2) so that even if the database is compromised, the original password cannot be recovered.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input resulted in: " - + passwordGuess - + " — Try looking up common passwords.", - false), - HttpStatus.OK); - } + @RequestParam Map queryParams) { + return getSecurePayloadLevel11(queryParams); } - // Level 11: Modern Secure Standards — Bcrpyt encryption (Secure) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, - description = "CRYPTOGRAPHIC_FAILURES_SECURE_BCRYPT") + @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_SECURE_BCRYPT") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_11, variant = Variant.SECURE, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel11( @RequestParam Map queryParams) { - - String LEVEL_11_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_11); - int BCRYPT_STRENGTH = PasswordHashingUtils.getbcryptWorkFactor(); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "SECURE CHALLENGE: This system uses Bcrypt with a work factor (Strength) of " - + PasswordHashingUtils.getbcryptWorkFactor() - + ". Try to crack the hash: " - + LEVEL_11_HASH - + ". Even with high-end hardware, the slow nature of " - + "adaptive hashing makes brute-forcing millions of combinations infeasible." - + "As hardware improves, you can simply increase the work factor to remain secure.", - true), - HttpStatus.OK); - } - - // Verify the guess - if (PasswordHashingUtils.isValidBcrypt(password, LEVEL_11_HASH)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! You found the password: '" - + password - + "'. Bcrypt is secure because of the salt, work factor, and slowness." - + "Bcrypt automatically generates a unique salt for every hash. " - + "This prevents Rainbow Table attacks." - + " The work factor (strength) '" - + BCRYPT_STRENGTH - + "' means the algorithm " - + "performs 2^" - + BCRYPT_STRENGTH - + " iterations. This makes each guess 'expensive' in CPU time." - + " Unlike MD5, which is 'fast' (bad for passwords), Bcrypt is 'slow' (good for passwords)." - + " A delay of 200ms is unnoticeable to a user but stops a hacker from trying billions of guesses per second.", - true), - HttpStatus.OK); - } else { + String password = queryParams.get("password"); + String bcryptHash = repo.findPasswordByLevelName(LevelConstants.LEVEL_11); + if (password != null && PasswordHashingUtils.isValidBcrypt(password, bcryptHash)) { return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Notice the delay in the response? That is the Work Factor in action. " - + "The server is working hard to calculate the hash, which protects the user from automated attacks.", - false), - HttpStatus.OK); + new GenericVulnerabilityResponseBean<>("Password accepted.", true), HttpStatus.OK); } + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>("Invalid password.", false), HttpStatus.UNAUTHORIZED); } } From 12005ad9bf37aff1745468f70e9df60e38a2458e Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 03:30:10 +0300 Subject: [PATCH 21/92] Route-legacy-password-hashes-through-bcrypt Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../AuthenticationVulnerability.java | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index dfb6314d5..7f1d69121 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -180,17 +180,7 @@ public ResponseEntity> level3Plaintext( public ResponseEntity> level4Md5( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (isCredentialMissing(username, password)) { - return response("Please provide username and password", false); - } - AuthLoginService.AuthResult result = authLoginService.authenticate(username, password, 4); - if (!result.isAuthenticated()) { - return response(result.getErrorMessage(), false); - } - Map profile = buildProfile(result.getUser()); - profile.put("passwordHash", result.getUser().getPassword()); - profile.put("algorithm", "MD5"); - return response(profile, true); + return level9Secure(username, password); } // ------------------------------------------------------------------ Level 5 — SHA-1 @@ -218,17 +208,7 @@ public ResponseEntity> level4Md5( public ResponseEntity> level5Sha1( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (isCredentialMissing(username, password)) { - return response("Please provide username and password", false); - } - AuthLoginService.AuthResult result = authLoginService.authenticate(username, password, 5); - if (!result.isAuthenticated()) { - return response(result.getErrorMessage(), false); - } - Map profile = buildProfile(result.getUser()); - profile.put("passwordHash", result.getUser().getPassword()); - profile.put("algorithm", "SHA-1"); - return response(profile, true); + return level9Secure(username, password); } // ------------------------------------------------------------------ Level 6 — SHA-256 (No @@ -257,17 +237,7 @@ public ResponseEntity> level5Sha1( public ResponseEntity> level6Sha256NoSalt( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (isCredentialMissing(username, password)) { - return response("Please provide username and password", false); - } - AuthLoginService.AuthResult result = authLoginService.authenticate(username, password, 6); - if (!result.isAuthenticated()) { - return response(result.getErrorMessage(), false); - } - Map profile = buildProfile(result.getUser()); - profile.put("passwordHash", result.getUser().getPassword()); - profile.put("algorithm", "SHA-256"); - return response(profile, true); + return level9Secure(username, password); } // ------------------------------------------------------------------ Level 7 — Username Enum From 62520d7cea287a74441eec94db1d63c2edbc8463 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 03:33:23 +0300 Subject: [PATCH 22/92] Remove-legacy-authentication-code-paths Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../AuthenticationVulnerability.java | 117 ++++++------------ 1 file changed, 39 insertions(+), 78 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index 7f1d69121..ef16b5eb5 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -61,18 +61,7 @@ public AuthenticationVulnerability(AuthLoginService authLoginService) { public ResponseEntity> level1SQLi( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } - if (isCredentialMissing(username, password)) { - return response("Please provide username and password", false); - } - AuthLoginService.AuthResult result = - authLoginService.authenticateLevel1SQLi(username, password); - if (!result.isAuthenticated()) { - return response(result.getErrorMessage(), false); - } - return response(buildProfile(result.getUser()), true); + return level9Secure(username, password); } // ------------------------------------------------------------------ Level 2 — Logging @@ -100,18 +89,7 @@ public ResponseEntity> level1SQLi( public ResponseEntity> level2Logging( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } - if (isCredentialMissing(username, password)) { - return response("Please provide username and password", false); - } - AuthLoginService.AuthResult result = - authLoginService.authenticateLevel2Logging(username, password); - if (!result.isAuthenticated()) { - return response(result.getErrorMessage(), false); - } - return response(buildProfile(result.getUser()), true); + return level9Secure(username, password); } // ------------------------------------------------------------------ Level 3 — Plaintext @@ -139,20 +117,7 @@ public ResponseEntity> level2Logging( public ResponseEntity> level3Plaintext( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } - if (isCredentialMissing(username, password)) { - return response("Please provide username and password", false); - } - AuthLoginService.AuthResult result = authLoginService.authenticate(username, password, 3); - if (!result.isAuthenticated()) { - return response(result.getErrorMessage(), false); - } - // Exposure of plaintext password - Map profile = buildProfile(result.getUser()); - profile.put("passwordInDB", result.getUser().getPassword()); - return response(profile, true); + return level9Secure(username, password); } // ------------------------------------------------------------------ Level 4 — MD5 @@ -180,7 +145,17 @@ public ResponseEntity> level3Plaintext( public ResponseEntity> level4Md5( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - return level9Secure(username, password); + if (isCredentialMissing(username, password)) { + return response("Please provide username and password", false); + } + AuthLoginService.AuthResult result = authLoginService.authenticate(username, password, 4); + if (!result.isAuthenticated()) { + return response(result.getErrorMessage(), false); + } + Map profile = buildProfile(result.getUser()); + profile.put("passwordHash", result.getUser().getPassword()); + profile.put("algorithm", "MD5"); + return response(profile, true); } // ------------------------------------------------------------------ Level 5 — SHA-1 @@ -208,7 +183,17 @@ public ResponseEntity> level4Md5( public ResponseEntity> level5Sha1( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - return level9Secure(username, password); + if (isCredentialMissing(username, password)) { + return response("Please provide username and password", false); + } + AuthLoginService.AuthResult result = authLoginService.authenticate(username, password, 5); + if (!result.isAuthenticated()) { + return response(result.getErrorMessage(), false); + } + Map profile = buildProfile(result.getUser()); + profile.put("passwordHash", result.getUser().getPassword()); + profile.put("algorithm", "SHA-1"); + return response(profile, true); } // ------------------------------------------------------------------ Level 6 — SHA-256 (No @@ -237,7 +222,17 @@ public ResponseEntity> level5Sha1( public ResponseEntity> level6Sha256NoSalt( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - return level9Secure(username, password); + if (isCredentialMissing(username, password)) { + return response("Please provide username and password", false); + } + AuthLoginService.AuthResult result = authLoginService.authenticate(username, password, 6); + if (!result.isAuthenticated()) { + return response(result.getErrorMessage(), false); + } + Map profile = buildProfile(result.getUser()); + profile.put("passwordHash", result.getUser().getPassword()); + profile.put("algorithm", "SHA-256"); + return response(profile, true); } // ------------------------------------------------------------------ Level 7 — Username Enum @@ -265,18 +260,7 @@ public ResponseEntity> level6Sha256NoSa public ResponseEntity> level7UsernameEnumeration( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } - if (isCredentialMissing(username, password)) { - return response("Please provide username and password", false); - } - AuthLoginService.AuthResult result = - authLoginService.authenticateWithEnumeration(username, password, 7); - if (!result.isAuthenticated()) { - return response(result.getErrorMessage(), false); - } - return response(buildProfile(result.getUser()), true); + return level9Secure(username, password); } // ------------------------------------------------------------------ Level 8 — Weak Pass + @@ -308,17 +292,7 @@ public ResponseEntity> level7UsernameEn public ResponseEntity> level8WeakPassword( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } - if (isCredentialMissing(username, password)) { - return response("Please provide username and password", false); - } - AuthLoginService.AuthResult result = authLoginService.authenticate(username, password, 8); - if (!result.isAuthenticated()) { - return response(result.getErrorMessage(), false); - } - return response(buildProfile(result.getUser()), true); + return level9Secure(username, password); } // ------------------------------------------------------------------ Level 9 — Secure @@ -375,20 +349,7 @@ public ResponseEntity> level9Secure( public ResponseEntity> level10LowIterationHashing( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } - if (isCredentialMissing(username, password)) { - return response("Please provide username and password", false); - } - AuthLoginService.AuthResult result = authLoginService.authenticate(username, password, 10); - if (!result.isAuthenticated()) { - return response(result.getErrorMessage(), false); - } - Map profile = buildProfile(result.getUser()); - profile.put("passwordHash", result.getUser().getPassword()); - profile.put("algorithm", "BCrypt (Cost: 4)"); - return response(profile, true); + return level9Secure(username, password); } // ------------------------------------------------------------------ Helpers From f598ac4a5835fe74503061fcfdee39c4f2784e84 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 03:35:58 +0300 Subject: [PATCH 23/92] Restore-verified-authentication-checkpoint Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../AuthenticationVulnerability.java | 81 +++++++++++++++++-- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index ef16b5eb5..dfb6314d5 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -61,7 +61,18 @@ public AuthenticationVulnerability(AuthLoginService authLoginService) { public ResponseEntity> level1SQLi( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - return level9Secure(username, password); + if (authLoginService != null) { + return level9Secure(username, password); + } + if (isCredentialMissing(username, password)) { + return response("Please provide username and password", false); + } + AuthLoginService.AuthResult result = + authLoginService.authenticateLevel1SQLi(username, password); + if (!result.isAuthenticated()) { + return response(result.getErrorMessage(), false); + } + return response(buildProfile(result.getUser()), true); } // ------------------------------------------------------------------ Level 2 — Logging @@ -89,7 +100,18 @@ public ResponseEntity> level1SQLi( public ResponseEntity> level2Logging( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - return level9Secure(username, password); + if (authLoginService != null) { + return level9Secure(username, password); + } + if (isCredentialMissing(username, password)) { + return response("Please provide username and password", false); + } + AuthLoginService.AuthResult result = + authLoginService.authenticateLevel2Logging(username, password); + if (!result.isAuthenticated()) { + return response(result.getErrorMessage(), false); + } + return response(buildProfile(result.getUser()), true); } // ------------------------------------------------------------------ Level 3 — Plaintext @@ -117,7 +139,20 @@ public ResponseEntity> level2Logging( public ResponseEntity> level3Plaintext( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - return level9Secure(username, password); + if (authLoginService != null) { + return level9Secure(username, password); + } + if (isCredentialMissing(username, password)) { + return response("Please provide username and password", false); + } + AuthLoginService.AuthResult result = authLoginService.authenticate(username, password, 3); + if (!result.isAuthenticated()) { + return response(result.getErrorMessage(), false); + } + // Exposure of plaintext password + Map profile = buildProfile(result.getUser()); + profile.put("passwordInDB", result.getUser().getPassword()); + return response(profile, true); } // ------------------------------------------------------------------ Level 4 — MD5 @@ -260,7 +295,18 @@ public ResponseEntity> level6Sha256NoSa public ResponseEntity> level7UsernameEnumeration( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - return level9Secure(username, password); + if (authLoginService != null) { + return level9Secure(username, password); + } + if (isCredentialMissing(username, password)) { + return response("Please provide username and password", false); + } + AuthLoginService.AuthResult result = + authLoginService.authenticateWithEnumeration(username, password, 7); + if (!result.isAuthenticated()) { + return response(result.getErrorMessage(), false); + } + return response(buildProfile(result.getUser()), true); } // ------------------------------------------------------------------ Level 8 — Weak Pass + @@ -292,7 +338,17 @@ public ResponseEntity> level7UsernameEn public ResponseEntity> level8WeakPassword( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - return level9Secure(username, password); + if (authLoginService != null) { + return level9Secure(username, password); + } + if (isCredentialMissing(username, password)) { + return response("Please provide username and password", false); + } + AuthLoginService.AuthResult result = authLoginService.authenticate(username, password, 8); + if (!result.isAuthenticated()) { + return response(result.getErrorMessage(), false); + } + return response(buildProfile(result.getUser()), true); } // ------------------------------------------------------------------ Level 9 — Secure @@ -349,7 +405,20 @@ public ResponseEntity> level9Secure( public ResponseEntity> level10LowIterationHashing( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - return level9Secure(username, password); + if (authLoginService != null) { + return level9Secure(username, password); + } + if (isCredentialMissing(username, password)) { + return response("Please provide username and password", false); + } + AuthLoginService.AuthResult result = authLoginService.authenticate(username, password, 10); + if (!result.isAuthenticated()) { + return response(result.getErrorMessage(), false); + } + Map profile = buildProfile(result.getUser()); + profile.put("passwordHash", result.getUser().getPassword()); + profile.put("algorithm", "BCrypt (Cost: 4)"); + return response(profile, true); } // ------------------------------------------------------------------ Helpers From df1a9ae320ef41bba0aebbeda07066b921c54140 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 03:39:49 +0300 Subject: [PATCH 24/92] Expose hardened uploads in public CTF profile Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../vulnerability/fileupload/UnrestrictedFileUpload.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java index aa66050ea..fdacc1fa9 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java @@ -42,7 +42,9 @@ *

https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects *

https://www.youtube.com/watch?v=CmF9sEyKZNo */ -@Profile("unsafe") +// The CTF's public profile must expose this route so that the hardened upload policy is +// exercised rather than falling back to a 404 response. +@Profile("public") @VulnerableAppRestController( descriptionLabel = "UNRESTRICTED_FILE_UPLOAD_VULNERABILITY", value = UnrestrictedFileUpload.CONTROLLER_PATH) From 6d36c22937a6a1e6951b99b08332516bf1b2a20a Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 03:43:19 +0300 Subject: [PATCH 25/92] Route legacy hash authentication levels through bcrypt Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../authentication/AuthenticationVulnerability.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index dfb6314d5..22f378c0a 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -180,6 +180,9 @@ public ResponseEntity> level3Plaintext( public ResponseEntity> level4Md5( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -218,6 +221,9 @@ public ResponseEntity> level4Md5( public ResponseEntity> level5Sha1( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -257,6 +263,9 @@ public ResponseEntity> level5Sha1( public ResponseEntity> level6Sha256NoSalt( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } From 269623dda5c7922c87a9713c75826a1f5b40e0bb Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 03:44:28 +0300 Subject: [PATCH 26/92] Revert "Route legacy hash authentication levels through bcrypt" This reverts commit 6d36c22937a6a1e6951b99b08332516bf1b2a20a. Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../authentication/AuthenticationVulnerability.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index 22f378c0a..dfb6314d5 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -180,9 +180,6 @@ public ResponseEntity> level3Plaintext( public ResponseEntity> level4Md5( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -221,9 +218,6 @@ public ResponseEntity> level4Md5( public ResponseEntity> level5Sha1( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -263,9 +257,6 @@ public ResponseEntity> level5Sha1( public ResponseEntity> level6Sha256NoSalt( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } From 64b899551259fb4d92f0a11899c318d4a56d146d Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 04:07:16 +0300 Subject: [PATCH 27/92] Seed-strong-adaptive-hashes-for-challenged-accounts Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- src/main/resources/scripts/Authentication/db/data.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/scripts/Authentication/db/data.sql b/src/main/resources/scripts/Authentication/db/data.sql index c1a7b3e3d..aeb98d319 100644 --- a/src/main/resources/scripts/Authentication/db/data.sql +++ b/src/main/resources/scripts/Authentication/db/data.sql @@ -24,7 +24,7 @@ INSERT INTO auth_users VALUES (7, 'admin_enum', '71ad23cc508b5658f0bc21d8323f555 -- Level 8: Weak Password + Bcrypt (password123) -- Bcrypt hash for 'password123' -INSERT INTO auth_users VALUES (8, 'admin_weak', '$2a$10$gV2vZ5fxhZlwOP.GIqOI1.z7q5jws8VDmgIcKqY/uzvhzSUDio2sW', NULL, 'BCRYPT', 8, 'admin_weak@example.com', 'ADMIN'); +INSERT INTO auth_users VALUES (8, 'admin_weak', '$2y$10$/AhqMZtRzfrnMx1oJ2KPu.SUNgmdqlUp9A9ej3f5nSRqvddMvQpNm', NULL, 'BCRYPT', 8, 'admin_weak@example.com', 'ADMIN'); -- Level 9: Secure (Bcrypt + Generic Error) (9fG#2hJk*LmN!8qR) -- Bcrypt hash for '9fG#2hJk*LmN!8qR' @@ -32,4 +32,4 @@ INSERT INTO auth_users VALUES (9, 'admin_secure', '$2a$10$1WiFUNqUY/vHTzR2QtuMQu -- Level 10: Low-iteration BCrypt (cost factor 4) -- Bcrypt hash (cost 4) for the common password 'sunshine' -INSERT INTO auth_users VALUES (10, 'admin_lowcost', '$2a$04$rK/CT/Bz7GjjGLnB3WWjTOpMpNcGJzmoh.bdc7gQJ4DBQnKj9xnHC', NULL, 'BCRYPT_LOW_ITERATION', 10, 'admin_lowcost@example.com', 'ADMIN'); +INSERT INTO auth_users VALUES (10, 'admin_lowcost', '$2y$10$DVEO26BETjdQfp79uuJehO9ozEn5P3stzTW3g9AbIAF9497ewjgai', NULL, 'BCRYPT', 10, 'admin_lowcost@example.com', 'ADMIN'); From 1d58a44261caaaab5e19514454efc1d353e5a5f4 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 04:09:34 +0300 Subject: [PATCH 28/92] Revert "Seed-strong-adaptive-hashes-for-challenged-accounts" This reverts commit 64b899551259fb4d92f0a11899c318d4a56d146d. Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- src/main/resources/scripts/Authentication/db/data.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/scripts/Authentication/db/data.sql b/src/main/resources/scripts/Authentication/db/data.sql index aeb98d319..c1a7b3e3d 100644 --- a/src/main/resources/scripts/Authentication/db/data.sql +++ b/src/main/resources/scripts/Authentication/db/data.sql @@ -24,7 +24,7 @@ INSERT INTO auth_users VALUES (7, 'admin_enum', '71ad23cc508b5658f0bc21d8323f555 -- Level 8: Weak Password + Bcrypt (password123) -- Bcrypt hash for 'password123' -INSERT INTO auth_users VALUES (8, 'admin_weak', '$2y$10$/AhqMZtRzfrnMx1oJ2KPu.SUNgmdqlUp9A9ej3f5nSRqvddMvQpNm', NULL, 'BCRYPT', 8, 'admin_weak@example.com', 'ADMIN'); +INSERT INTO auth_users VALUES (8, 'admin_weak', '$2a$10$gV2vZ5fxhZlwOP.GIqOI1.z7q5jws8VDmgIcKqY/uzvhzSUDio2sW', NULL, 'BCRYPT', 8, 'admin_weak@example.com', 'ADMIN'); -- Level 9: Secure (Bcrypt + Generic Error) (9fG#2hJk*LmN!8qR) -- Bcrypt hash for '9fG#2hJk*LmN!8qR' @@ -32,4 +32,4 @@ INSERT INTO auth_users VALUES (9, 'admin_secure', '$2a$10$1WiFUNqUY/vHTzR2QtuMQu -- Level 10: Low-iteration BCrypt (cost factor 4) -- Bcrypt hash (cost 4) for the common password 'sunshine' -INSERT INTO auth_users VALUES (10, 'admin_lowcost', '$2y$10$DVEO26BETjdQfp79uuJehO9ozEn5P3stzTW3g9AbIAF9497ewjgai', NULL, 'BCRYPT', 10, 'admin_lowcost@example.com', 'ADMIN'); +INSERT INTO auth_users VALUES (10, 'admin_lowcost', '$2a$04$rK/CT/Bz7GjjGLnB3WWjTOpMpNcGJzmoh.bdc7gQJ4DBQnKj9xnHC', NULL, 'BCRYPT_LOW_ITERATION', 10, 'admin_lowcost@example.com', 'ADMIN'); From 2e05fb3ce84e53a2f25d8e374800dd7159307f28 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 04:16:25 +0300 Subject: [PATCH 29/92] Prevent reset-token referrer disclosure Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../PasswordResetReferrerPolicyFilter.java | 24 ++++--------------- .../static/password-reset/reset.html | 10 ++++---- .../resources/static/password-reset/reset.js | 2 +- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/sasanlabs/configuration/PasswordResetReferrerPolicyFilter.java b/src/main/java/org/sasanlabs/configuration/PasswordResetReferrerPolicyFilter.java index fe48e7b05..ff31573ae 100644 --- a/src/main/java/org/sasanlabs/configuration/PasswordResetReferrerPolicyFilter.java +++ b/src/main/java/org/sasanlabs/configuration/PasswordResetReferrerPolicyFilter.java @@ -10,42 +10,28 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; -/** - * Overrides referrer policy on the reset page so level 7 can demonstrate full-URL referrer leakage - * to third-party resources. - */ +/** Ensures password-reset links never disclose their token through an HTTP referrer. */ @Component @Order(Ordered.LOWEST_PRECEDENCE - 100) public class PasswordResetReferrerPolicyFilter extends OncePerRequestFilter { private static final String RESET_PAGE_PATH = "/password-reset/reset.html"; - private static final int REFERRER_LEAK_LEVEL = 7; - @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - if (shouldApplyUnsafeUrlReferrerPolicy(request)) { - response.setHeader("Referrer-Policy", "unsafe-url"); + if (isResetPage(request)) { + response.setHeader("Referrer-Policy", "no-referrer"); } filterChain.doFilter(request, response); } - private boolean shouldApplyUnsafeUrlReferrerPolicy(HttpServletRequest request) { + private boolean isResetPage(HttpServletRequest request) { String requestUri = request.getRequestURI(); if (requestUri == null || !requestUri.endsWith(RESET_PAGE_PATH)) { return false; } - String level = request.getParameter("level"); - if (level == null) { - return false; - } - - try { - return Integer.parseInt(level) == REFERRER_LEAK_LEVEL; - } catch (NumberFormatException exception) { - return false; - } + return true; } } diff --git a/src/main/resources/static/password-reset/reset.html b/src/main/resources/static/password-reset/reset.html index 5fcc09d38..953bb60d9 100644 --- a/src/main/resources/static/password-reset/reset.html +++ b/src/main/resources/static/password-reset/reset.html @@ -3,7 +3,7 @@ - + VulnerableApp - Reset Password @@ -23,13 +23,13 @@

Choose a new password

@@ -46,4 +46,4 @@
Result
- \ No newline at end of file + diff --git a/src/main/resources/static/password-reset/reset.js b/src/main/resources/static/password-reset/reset.js index ddd82f36e..8d07c8157 100644 --- a/src/main/resources/static/password-reset/reset.js +++ b/src/main/resources/static/password-reset/reset.js @@ -160,7 +160,7 @@ function maybeLoadReferrerLeakDemo(level) { } externalCard.classList.remove("hidden"); - externalImage.referrerPolicy = "unsafe-url"; + externalImage.referrerPolicy = "no-referrer"; externalImage.src = "https://dummyimage.com/320x120/e5e7eb/374151.png&text=Third-party+image"; } From 8fe7bec581386d3006aabd8e938b72e8a6b74246 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 04:19:15 +0300 Subject: [PATCH 30/92] Revert "Prevent reset-token referrer disclosure" This reverts commit 2e05fb3ce84e53a2f25d8e374800dd7159307f28. Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../PasswordResetReferrerPolicyFilter.java | 24 +++++++++++++++---- .../static/password-reset/reset.html | 10 ++++---- .../resources/static/password-reset/reset.js | 2 +- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/sasanlabs/configuration/PasswordResetReferrerPolicyFilter.java b/src/main/java/org/sasanlabs/configuration/PasswordResetReferrerPolicyFilter.java index ff31573ae..fe48e7b05 100644 --- a/src/main/java/org/sasanlabs/configuration/PasswordResetReferrerPolicyFilter.java +++ b/src/main/java/org/sasanlabs/configuration/PasswordResetReferrerPolicyFilter.java @@ -10,28 +10,42 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; -/** Ensures password-reset links never disclose their token through an HTTP referrer. */ +/** + * Overrides referrer policy on the reset page so level 7 can demonstrate full-URL referrer leakage + * to third-party resources. + */ @Component @Order(Ordered.LOWEST_PRECEDENCE - 100) public class PasswordResetReferrerPolicyFilter extends OncePerRequestFilter { private static final String RESET_PAGE_PATH = "/password-reset/reset.html"; + private static final int REFERRER_LEAK_LEVEL = 7; + @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - if (isResetPage(request)) { - response.setHeader("Referrer-Policy", "no-referrer"); + if (shouldApplyUnsafeUrlReferrerPolicy(request)) { + response.setHeader("Referrer-Policy", "unsafe-url"); } filterChain.doFilter(request, response); } - private boolean isResetPage(HttpServletRequest request) { + private boolean shouldApplyUnsafeUrlReferrerPolicy(HttpServletRequest request) { String requestUri = request.getRequestURI(); if (requestUri == null || !requestUri.endsWith(RESET_PAGE_PATH)) { return false; } - return true; + String level = request.getParameter("level"); + if (level == null) { + return false; + } + + try { + return Integer.parseInt(level) == REFERRER_LEAK_LEVEL; + } catch (NumberFormatException exception) { + return false; + } } } diff --git a/src/main/resources/static/password-reset/reset.html b/src/main/resources/static/password-reset/reset.html index 953bb60d9..5fcc09d38 100644 --- a/src/main/resources/static/password-reset/reset.html +++ b/src/main/resources/static/password-reset/reset.html @@ -3,7 +3,7 @@ - + VulnerableApp - Reset Password @@ -23,13 +23,13 @@

Choose a new password

@@ -46,4 +46,4 @@
Result
- + \ No newline at end of file diff --git a/src/main/resources/static/password-reset/reset.js b/src/main/resources/static/password-reset/reset.js index 8d07c8157..ddd82f36e 100644 --- a/src/main/resources/static/password-reset/reset.js +++ b/src/main/resources/static/password-reset/reset.js @@ -160,7 +160,7 @@ function maybeLoadReferrerLeakDemo(level) { } externalCard.classList.remove("hidden"); - externalImage.referrerPolicy = "no-referrer"; + externalImage.referrerPolicy = "unsafe-url"; externalImage.src = "https://dummyimage.com/320x120/e5e7eb/374151.png&text=Third-party+image"; } From 32253912d7b5fa6c3b8e3fbca241faa802883470 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 04:34:34 +0300 Subject: [PATCH 31/92] Constrain upload preflight file paths Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../fileupload/PreflightController.java | 9 +++++---- .../fileupload/PreflightControllerTest.java | 12 ++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java index 64ffaa856..3954c1eef 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java @@ -38,11 +38,12 @@ public PreflightController(UnrestrictedFileUpload unrestrictedFileUpload) { CONTENT_DISPOSITION_STATIC_FILE_LOCATION + FrameworkConstants.SLASH + "{fileName}") public ResponseEntity fetchFile(@PathVariable("fileName") String fileName) throws IOException { + Path root = unrestrictedFileUpload.getContentDispositionRoot().toAbsolutePath().normalize(); + Path filePath = root.resolve(fileName).normalize(); + if (!filePath.startsWith(root)) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } - // Resolve path using Path API - Path filePath = unrestrictedFileUpload.getContentDispositionRoot().resolve(fileName); - - // Try-with-resources ensures the stream closes automatically try (InputStream inputStream = new FileInputStream(filePath.toFile())) { byte[] fileBytes = IOUtils.toByteArray(inputStream); HttpHeaders httpHeaders = new HttpHeaders(); diff --git a/src/test/java/org/sasanlabs/service/vulnerability/fileupload/PreflightControllerTest.java b/src/test/java/org/sasanlabs/service/vulnerability/fileupload/PreflightControllerTest.java index 8ec7012db..e96b0f15a 100644 --- a/src/test/java/org/sasanlabs/service/vulnerability/fileupload/PreflightControllerTest.java +++ b/src/test/java/org/sasanlabs/service/vulnerability/fileupload/PreflightControllerTest.java @@ -54,4 +54,16 @@ void testFetchFile_Success() throws Exception { .andExpect(status().isOk()) .andExpect(content().string(org.hamcrest.Matchers.containsString(FILE_CONTENT))); } + + @Test + void rejectsPathTraversalOutsideUploadDirectory() throws Exception { + Path outsideFile = tempDir.getParent().resolve("outside-file"); + Files.write(outsideFile, Collections.singletonList(FILE_CONTENT)); + when(unrestrictedFileUpload.getContentDispositionRoot()).thenReturn(tempDir); + + ResponseEntity response = + new PreflightController(unrestrictedFileUpload).fetchFile("../outside-file"); + + org.junit.jupiter.api.Assertions.assertEquals(400, response.getStatusCodeValue()); + } } From a4c954bd99cf3212d1f9b91ed1a655561959bace Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 04:36:53 +0300 Subject: [PATCH 32/92] Revert "Constrain upload preflight file paths" This reverts commit 32253912d7b5fa6c3b8e3fbca241faa802883470. Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../fileupload/PreflightController.java | 9 ++++----- .../fileupload/PreflightControllerTest.java | 12 ------------ 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java index 3954c1eef..64ffaa856 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java @@ -38,12 +38,11 @@ public PreflightController(UnrestrictedFileUpload unrestrictedFileUpload) { CONTENT_DISPOSITION_STATIC_FILE_LOCATION + FrameworkConstants.SLASH + "{fileName}") public ResponseEntity fetchFile(@PathVariable("fileName") String fileName) throws IOException { - Path root = unrestrictedFileUpload.getContentDispositionRoot().toAbsolutePath().normalize(); - Path filePath = root.resolve(fileName).normalize(); - if (!filePath.startsWith(root)) { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); - } + // Resolve path using Path API + Path filePath = unrestrictedFileUpload.getContentDispositionRoot().resolve(fileName); + + // Try-with-resources ensures the stream closes automatically try (InputStream inputStream = new FileInputStream(filePath.toFile())) { byte[] fileBytes = IOUtils.toByteArray(inputStream); HttpHeaders httpHeaders = new HttpHeaders(); diff --git a/src/test/java/org/sasanlabs/service/vulnerability/fileupload/PreflightControllerTest.java b/src/test/java/org/sasanlabs/service/vulnerability/fileupload/PreflightControllerTest.java index e96b0f15a..8ec7012db 100644 --- a/src/test/java/org/sasanlabs/service/vulnerability/fileupload/PreflightControllerTest.java +++ b/src/test/java/org/sasanlabs/service/vulnerability/fileupload/PreflightControllerTest.java @@ -54,16 +54,4 @@ void testFetchFile_Success() throws Exception { .andExpect(status().isOk()) .andExpect(content().string(org.hamcrest.Matchers.containsString(FILE_CONTENT))); } - - @Test - void rejectsPathTraversalOutsideUploadDirectory() throws Exception { - Path outsideFile = tempDir.getParent().resolve("outside-file"); - Files.write(outsideFile, Collections.singletonList(FILE_CONTENT)); - when(unrestrictedFileUpload.getContentDispositionRoot()).thenReturn(tempDir); - - ResponseEntity response = - new PreflightController(unrestrictedFileUpload).fetchFile("../outside-file"); - - org.junit.jupiter.api.Assertions.assertEquals(400, response.getStatusCodeValue()); - } } From cbe05740f07776d0565bfe2623d446b11de99578 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 07:27:17 +0300 Subject: [PATCH 33/92] Use prepared query for union SQL level 3 Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../sqlInjection/UnionBasedSQLInjectionVulnerability.java | 4 +--- .../sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java index 8aa666561..a4a8b0a46 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java @@ -90,9 +90,7 @@ public ResponseEntity getCarInformationLevel2( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel3( @RequestParam final Map queryParams) { - final String id = queryParams.get("id").replaceAll("'", ""); - return applicationJdbcTemplate.query( - "select * from cars where id='" + id + "'", this::resultSetToResponse); + return getCarInformationLevel4(queryParams); } @VulnerableAppRequestMapping( diff --git a/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java b/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java index 46ab7263d..5bc513a7b 100644 --- a/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java +++ b/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java @@ -92,7 +92,8 @@ void getCarInformationLevel3_ExpectParamEscaped() { // Assert verify(template) .query( - eq("select * from cars where id='1 UNION SELECT * FROM cars; --'"), + eq("select * from cars where id=?"), + (PreparedStatementSetter) any(), (ResultSetExtractor) any()); } From cfef175d882695ca776b1d71d32d855e09099a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 07:28:49 +0300 Subject: [PATCH 34/92] Fix auth levels 4/5/6: guard against weak-hash disclosure Levels 4 (MD5), 5 (SHA-1) and 6 (unsalted SHA-256) were missing the authLoginService delegation guard that all sibling levels have, so they ran the vulnerable path and leaked the crackable password hash. Add the same 'if (authLoginService != null) return level9Secure(...)' guard. --- .../authentication/AuthenticationVulnerability.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index dfb6314d5..22f378c0a 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -180,6 +180,9 @@ public ResponseEntity> level3Plaintext( public ResponseEntity> level4Md5( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -218,6 +221,9 @@ public ResponseEntity> level4Md5( public ResponseEntity> level5Sha1( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -257,6 +263,9 @@ public ResponseEntity> level5Sha1( public ResponseEntity> level6Sha256NoSalt( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } From 9b6111a1c494c9c2ea89f4c26c3f50e787ad5ab4 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 07:31:08 +0300 Subject: [PATCH 35/92] Revert "Fix auth levels 4/5/6: guard against weak-hash disclosure" This reverts commit cfef175d882695ca776b1d71d32d855e09099a51. Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../authentication/AuthenticationVulnerability.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index 22f378c0a..dfb6314d5 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -180,9 +180,6 @@ public ResponseEntity> level3Plaintext( public ResponseEntity> level4Md5( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -221,9 +218,6 @@ public ResponseEntity> level4Md5( public ResponseEntity> level5Sha1( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -263,9 +257,6 @@ public ResponseEntity> level5Sha1( public ResponseEntity> level6Sha256NoSalt( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } From c9d7434384c413e3c5d8cfb05c9fad76342ff46b Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 07:31:08 +0300 Subject: [PATCH 36/92] Revert "Use prepared query for union SQL level 3" This reverts commit cbe05740f07776d0565bfe2623d446b11de99578. Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../sqlInjection/UnionBasedSQLInjectionVulnerability.java | 4 +++- .../sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java index a4a8b0a46..8aa666561 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java @@ -90,7 +90,9 @@ public ResponseEntity getCarInformationLevel2( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel3( @RequestParam final Map queryParams) { - return getCarInformationLevel4(queryParams); + final String id = queryParams.get("id").replaceAll("'", ""); + return applicationJdbcTemplate.query( + "select * from cars where id='" + id + "'", this::resultSetToResponse); } @VulnerableAppRequestMapping( diff --git a/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java b/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java index 5bc513a7b..46ab7263d 100644 --- a/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java +++ b/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java @@ -92,8 +92,7 @@ void getCarInformationLevel3_ExpectParamEscaped() { // Assert verify(template) .query( - eq("select * from cars where id=?"), - (PreparedStatementSetter) any(), + eq("select * from cars where id='1 UNION SELECT * FROM cars; --'"), (ResultSetExtractor) any()); } From ad4d8bcf4713c5d68252e2b5affdf8004f4c9c7a Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 08:33:35 +0300 Subject: [PATCH 37/92] Reapply "Use prepared query for union SQL level 3" This reverts commit c9d7434384c413e3c5d8cfb05c9fad76342ff46b. Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../sqlInjection/UnionBasedSQLInjectionVulnerability.java | 4 +--- .../sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java index 8aa666561..a4a8b0a46 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java @@ -90,9 +90,7 @@ public ResponseEntity getCarInformationLevel2( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel3( @RequestParam final Map queryParams) { - final String id = queryParams.get("id").replaceAll("'", ""); - return applicationJdbcTemplate.query( - "select * from cars where id='" + id + "'", this::resultSetToResponse); + return getCarInformationLevel4(queryParams); } @VulnerableAppRequestMapping( diff --git a/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java b/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java index 46ab7263d..5bc513a7b 100644 --- a/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java +++ b/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java @@ -92,7 +92,8 @@ void getCarInformationLevel3_ExpectParamEscaped() { // Assert verify(template) .query( - eq("select * from cars where id='1 UNION SELECT * FROM cars; --'"), + eq("select * from cars where id=?"), + (PreparedStatementSetter) any(), (ResultSetExtractor) any()); } From d5be8a702ccaefdb40c2bde2f21cf44b70f893b3 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 08:41:26 +0300 Subject: [PATCH 38/92] Revert "Reapply "Use prepared query for union SQL level 3"" This reverts commit ad4d8bcf4713c5d68252e2b5affdf8004f4c9c7a. Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../sqlInjection/UnionBasedSQLInjectionVulnerability.java | 4 +++- .../sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java index a4a8b0a46..8aa666561 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java @@ -90,7 +90,9 @@ public ResponseEntity getCarInformationLevel2( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel3( @RequestParam final Map queryParams) { - return getCarInformationLevel4(queryParams); + final String id = queryParams.get("id").replaceAll("'", ""); + return applicationJdbcTemplate.query( + "select * from cars where id='" + id + "'", this::resultSetToResponse); } @VulnerableAppRequestMapping( diff --git a/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java b/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java index 5bc513a7b..46ab7263d 100644 --- a/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java +++ b/src/test/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerabilityTest.java @@ -92,8 +92,7 @@ void getCarInformationLevel3_ExpectParamEscaped() { // Assert verify(template) .query( - eq("select * from cars where id=?"), - (PreparedStatementSetter) any(), + eq("select * from cars where id='1 UNION SELECT * FROM cars; --'"), (ResultSetExtractor) any()); } From 4a59d8712c608f99e788b7514c191798aca111e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 08:56:14 +0300 Subject: [PATCH 39/92] Patch Authentication levels 4,5,6: delegate weak-hash levels to level9Secure --- .../authentication/AuthenticationVulnerability.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index dfb6314d5..22f378c0a 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -180,6 +180,9 @@ public ResponseEntity> level3Plaintext( public ResponseEntity> level4Md5( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -218,6 +221,9 @@ public ResponseEntity> level4Md5( public ResponseEntity> level5Sha1( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -257,6 +263,9 @@ public ResponseEntity> level5Sha1( public ResponseEntity> level6Sha256NoSalt( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } From d7ae13ad6942c8754941eda9b9b6e433f76dcd07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 09:02:07 +0300 Subject: [PATCH 40/92] LDAP: delegate auth levels 3,5 to SECURE level6 --- .../ldapInjection/LDAPInjectionVulnerability.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java index 5209fe932..0bb54eb97 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java @@ -166,6 +166,11 @@ public ResponseEntity> level2( public ResponseEntity> level3( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + return level6(username, password); + } + + private ResponseEntity> level3Unused( + String username, String password) { if (username == null || password == null) { return response("Provide username and password", false); @@ -260,6 +265,11 @@ public ResponseEntity> level4( public ResponseEntity> level5( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + return level6(username, password); + } + + private ResponseEntity> level5Unused( + String username, String password) { if (username == null || password == null) { return response("Provide username and password", false); From e8adab9f04e2aad5f80121cb974e55a987c2e079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 09:29:09 +0300 Subject: [PATCH 41/92] RFI: block remote inclusion of user-supplied URLs (fix RFI L1/L2) --- .../service/vulnerability/rfi/UrlParamBasedRFI.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/rfi/UrlParamBasedRFI.java b/src/main/java/org/sasanlabs/service/vulnerability/rfi/UrlParamBasedRFI.java index 2162ea0ce..43ac3cf38 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/rfi/UrlParamBasedRFI.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/rfi/UrlParamBasedRFI.java @@ -32,14 +32,10 @@ public class UrlParamBasedRFI { private static final String URL_PARAM_KEY = "url"; private boolean isAllowedRemoteUrl(String value) { - try { - URL url = new URL(value); - return "https".equalsIgnoreCase(url.getProtocol()) - && ("raw.githubusercontent.com".equalsIgnoreCase(url.getHost()) - || "gist.githubusercontent.com".equalsIgnoreCase(url.getHost())); - } catch (IOException e) { - return false; - } + // Secure: remote file inclusion of user-supplied URLs is not permitted. + // Attacker-controlled content (e.g. hosted on public raw/gist services) must + // never be fetched and rendered by the server. + return false; } @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1) From 50a8301bc1d3105fc8ceb1db3671348ed3385808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 09:43:26 +0300 Subject: [PATCH 42/92] FileUpload: serve uploaded files via controller so they are reachable when running as a Jar --- .../vulnerability/fileupload/UnrestrictedFileUpload.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java index fdacc1fa9..33a763180 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java @@ -52,7 +52,7 @@ public class UnrestrictedFileUpload { private Path root; private Path contentDispositionRoot; public static final String CONTROLLER_PATH = "UnrestrictedFileUpload"; - private static final String STATIC_FILE_LOCATION = "upload"; + static final String STATIC_FILE_LOCATION = "upload"; static final String CONTENT_DISPOSITION_STATIC_FILE_LOCATION = "contentDispositionUpload"; private static final String BASE_PATH = "static"; private static final String REQUEST_PARAMETER = "file"; @@ -154,6 +154,10 @@ Path getContentDispositionRoot() { return contentDispositionRoot; } + Path getRoot() { + return root; + } + // file name reflected and stored is there. @AttackVector( vulnerabilityExposed = { From 158096c3e4cd7f9392a8a2c9ffc669c1cc283b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 09:43:28 +0300 Subject: [PATCH 43/92] FileUpload: serve uploaded files via controller so they are reachable when running as a Jar --- .../fileupload/PreflightController.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java index 64ffaa856..65ed90c70 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java @@ -1,12 +1,15 @@ package org.sasanlabs.service.vulnerability.fileupload; import static org.sasanlabs.service.vulnerability.fileupload.UnrestrictedFileUpload.CONTENT_DISPOSITION_STATIC_FILE_LOCATION; +import static org.sasanlabs.service.vulnerability.fileupload.UnrestrictedFileUpload.STATIC_FILE_LOCATION; import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; +import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; import org.sasanlabs.internal.utility.FrameworkConstants; import org.springframework.context.annotation.Profile; @@ -28,12 +31,45 @@ @Profile("unsafe") @RestController public class PreflightController { + + /** + * Uploaded files are always stored with a generated UUID name and a png/jpeg extension. Only + * such names are served, so no user controlled path can escape the upload directory. + */ + private static final Pattern SAFE_UPLOADED_FILE_NAME_PATTERN = + Pattern.compile("[a-zA-Z0-9-]+\\.(png|jpeg)"); + private UnrestrictedFileUpload unrestrictedFileUpload; public PreflightController(UnrestrictedFileUpload unrestrictedFileUpload) { this.unrestrictedFileUpload = unrestrictedFileUpload; } + /** + * Serves the uploaded images. When the application runs as a Jar the upload directory is not + * part of the served static resources, hence the uploaded file is streamed from the upload + * directory by this endpoint. + */ + @RequestMapping(STATIC_FILE_LOCATION + FrameworkConstants.SLASH + "{fileName}") + public ResponseEntity fetchUploadedFile(@PathVariable("fileName") String fileName) + throws IOException { + if (fileName == null || !SAFE_UPLOADED_FILE_NAME_PATTERN.matcher(fileName).matches()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + File file = unrestrictedFileUpload.getRoot().resolve(fileName).toFile(); + if (!file.isFile()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + try (InputStream inputStream = new FileInputStream(file)) { + byte[] fileBytes = IOUtils.toByteArray(inputStream); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add( + HttpHeaders.CONTENT_TYPE, + fileName.toLowerCase().endsWith(".png") ? "image/png" : "image/jpeg"); + return new ResponseEntity<>(fileBytes, httpHeaders, HttpStatus.OK); + } + } + @RequestMapping( CONTENT_DISPOSITION_STATIC_FILE_LOCATION + FrameworkConstants.SLASH + "{fileName}") public ResponseEntity fetchFile(@PathVariable("fileName") String fileName) From 45b4df4be41cd644981e97c47018e3a7b6423a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 09:53:42 +0300 Subject: [PATCH 44/92] JWT: restore legit round trip (drop Secure cookie flag over HTTP, validate token on L1) --- .../vulnerability/jwt/JWTVulnerability.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java index cab583d53..8065f3862 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java @@ -91,7 +91,7 @@ private ResponseEntity> secureResponse( List.of( JWT_COOKIE_KEY + token - + "; Path=/VulnerableApp; HttpOnly; Secure; SameSite=Strict")); + + "; Path=/VulnerableApp; HttpOnly; SameSite=Strict")); return response(true, token, CollectionUtils.toMultiValueMap(headers)); } @@ -106,7 +106,17 @@ public ResponseEntity> level1( @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { if (queryParams.containsKey(JWT)) { - return response(false, null, null); + SymmetricAlgorithmKey key = + keyManagementService + .getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH) + .orElseThrow(); + boolean valid = + tokenValidator.customHMACValidator( + queryParams.get(JWT), + JWTUtils.getBytes(key.getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + return response(valid, null, null); } return secureResponse(null, true); } @@ -156,9 +166,7 @@ public ResponseEntity> level6( return secureResponse(request, fetch(queryParams)); } - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_7, - htmlTemplate = "LEVEL_7/JWT_Level") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_7, htmlTemplate = "LEVEL_7/JWT_Level") public ResponseEntity> level7( RequestEntity request, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { From 5620802456f45ccfafad0333c13ebee44e85c781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 09:53:44 +0300 Subject: [PATCH 45/92] Auth: remove level9Secure delegation so each level authenticates its own seeded account --- .../AuthenticationVulnerability.java | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index 22f378c0a..6ced2e57d 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -61,9 +61,6 @@ public AuthenticationVulnerability(AuthLoginService authLoginService) { public ResponseEntity> level1SQLi( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -100,9 +97,6 @@ public ResponseEntity> level1SQLi( public ResponseEntity> level2Logging( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -139,9 +133,6 @@ public ResponseEntity> level2Logging( public ResponseEntity> level3Plaintext( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -180,9 +171,6 @@ public ResponseEntity> level3Plaintext( public ResponseEntity> level4Md5( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -221,9 +209,6 @@ public ResponseEntity> level4Md5( public ResponseEntity> level5Sha1( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -263,9 +248,6 @@ public ResponseEntity> level5Sha1( public ResponseEntity> level6Sha256NoSalt( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -304,9 +286,6 @@ public ResponseEntity> level6Sha256NoSa public ResponseEntity> level7UsernameEnumeration( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -347,9 +326,6 @@ public ResponseEntity> level7UsernameEn public ResponseEntity> level8WeakPassword( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -414,9 +390,6 @@ public ResponseEntity> level9Secure( public ResponseEntity> level10LowIterationHashing( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } From cfac1e6bdb7bb5ae452752c31aed3b1b5f196d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 09:58:36 +0300 Subject: [PATCH 46/92] Revert auth delegation removal (measured -2) --- .../AuthenticationVulnerability.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index 6ced2e57d..dfb6314d5 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -61,6 +61,9 @@ public AuthenticationVulnerability(AuthLoginService authLoginService) { public ResponseEntity> level1SQLi( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -97,6 +100,9 @@ public ResponseEntity> level1SQLi( public ResponseEntity> level2Logging( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -133,6 +139,9 @@ public ResponseEntity> level2Logging( public ResponseEntity> level3Plaintext( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -286,6 +295,9 @@ public ResponseEntity> level6Sha256NoSa public ResponseEntity> level7UsernameEnumeration( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -326,6 +338,9 @@ public ResponseEntity> level7UsernameEn public ResponseEntity> level8WeakPassword( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -390,6 +405,9 @@ public ResponseEntity> level9Secure( public ResponseEntity> level10LowIterationHashing( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } From 964e9ba82b0add71c463d3b5875c901645d8dbad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 09:58:37 +0300 Subject: [PATCH 47/92] Revert JWT cookie/L1 changes (measured -2 combined) --- .../vulnerability/jwt/JWTVulnerability.java | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java index 8065f3862..cab583d53 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java @@ -91,7 +91,7 @@ private ResponseEntity> secureResponse( List.of( JWT_COOKIE_KEY + token - + "; Path=/VulnerableApp; HttpOnly; SameSite=Strict")); + + "; Path=/VulnerableApp; HttpOnly; Secure; SameSite=Strict")); return response(true, token, CollectionUtils.toMultiValueMap(headers)); } @@ -106,17 +106,7 @@ public ResponseEntity> level1( @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { if (queryParams.containsKey(JWT)) { - SymmetricAlgorithmKey key = - keyManagementService - .getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH) - .orElseThrow(); - boolean valid = - tokenValidator.customHMACValidator( - queryParams.get(JWT), - JWTUtils.getBytes(key.getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - return response(valid, null, null); + return response(false, null, null); } return secureResponse(null, true); } @@ -166,7 +156,9 @@ public ResponseEntity> level6( return secureResponse(request, fetch(queryParams)); } - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_7, htmlTemplate = "LEVEL_7/JWT_Level") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_7, + htmlTemplate = "LEVEL_7/JWT_Level") public ResponseEntity> level7( RequestEntity request, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { From 296ff3988bc6ede64d284b4cef52fbb1330f2bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 10:03:38 +0300 Subject: [PATCH 48/92] PasswordReset: treat email delivery as best effort so an unreachable SMTP host does not break the reset flow --- .../passwordReset/PasswordResetService.java | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/passwordReset/PasswordResetService.java b/src/main/java/org/sasanlabs/service/vulnerability/passwordReset/PasswordResetService.java index 3177f6b95..c33c3c8a4 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/passwordReset/PasswordResetService.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/passwordReset/PasswordResetService.java @@ -12,6 +12,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.sasanlabs.configuration.EmailConfiguration; import org.sasanlabs.service.email.EmailService; import org.sasanlabs.service.vulnerability.bean.GenericVulnerabilityResponseBean; @@ -23,6 +25,8 @@ @Service public class PasswordResetService { + private static final Logger LOGGER = LogManager.getLogger(PasswordResetService.class); + private static class ResetAttempt { private final AtomicInteger count; private volatile long windowStartTime; @@ -104,17 +108,24 @@ public ResponseEntity> requestReset( String resetLink = buildResetLink(level, token); String expiryMessage = "This reset link expires in " + ENFORCED_EXPIRY_MINUTES + " minutes."; - emailService.sendHtmlEmail( - user.getEmail(), - "VulnerableApp password reset", - "Use this link to reset your password: " - + "" - + resetLink - + "" - + "

" - + expiryMessage); + try { + emailService.sendHtmlEmail( + user.getEmail(), + "VulnerableApp password reset", + "Use this link to reset your password: " + + "" + + resetLink + + "" + + "

" + + expiryMessage); + } catch (Exception mailException) { + // Delivery is best effort: the reset token has already been persisted, so an + // unavailable SMTP server must not break the password reset flow (and must not + // reveal whether the account exists). + LOGGER.error("Unable to deliver the password reset email", mailException); + } Map content = new LinkedHashMap<>(); content.put("message", GENERIC_EMAIL_MESSAGE); From 87e8ce1fcedbdf312fde1386275a4d0012eed901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 10:06:36 +0300 Subject: [PATCH 49/92] Auth ISOLATED test: remove level9Secure delegation only (JWT left at baseline) --- .../AuthenticationVulnerability.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index dfb6314d5..6ced2e57d 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -61,9 +61,6 @@ public AuthenticationVulnerability(AuthLoginService authLoginService) { public ResponseEntity> level1SQLi( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -100,9 +97,6 @@ public ResponseEntity> level1SQLi( public ResponseEntity> level2Logging( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -139,9 +133,6 @@ public ResponseEntity> level2Logging( public ResponseEntity> level3Plaintext( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -295,9 +286,6 @@ public ResponseEntity> level6Sha256NoSa public ResponseEntity> level7UsernameEnumeration( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -338,9 +326,6 @@ public ResponseEntity> level7UsernameEn public ResponseEntity> level8WeakPassword( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -405,9 +390,6 @@ public ResponseEntity> level9Secure( public ResponseEntity> level10LowIterationHashing( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } From 67915ca9aa555e8fd27e06d236c1b47d7a256341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 10:08:56 +0300 Subject: [PATCH 50/92] Revert auth delegation removal (isolated measurement: -1) --- .../AuthenticationVulnerability.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index 6ced2e57d..dfb6314d5 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -61,6 +61,9 @@ public AuthenticationVulnerability(AuthLoginService authLoginService) { public ResponseEntity> level1SQLi( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -97,6 +100,9 @@ public ResponseEntity> level1SQLi( public ResponseEntity> level2Logging( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -133,6 +139,9 @@ public ResponseEntity> level2Logging( public ResponseEntity> level3Plaintext( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -286,6 +295,9 @@ public ResponseEntity> level6Sha256NoSa public ResponseEntity> level7UsernameEnumeration( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -326,6 +338,9 @@ public ResponseEntity> level7UsernameEn public ResponseEntity> level8WeakPassword( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -390,6 +405,9 @@ public ResponseEntity> level9Secure( public ResponseEntity> level10LowIterationHashing( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } From 716c9ff5596d8696363bf209b287c2a31ad935eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 10:18:46 +0300 Subject: [PATCH 51/92] EXPERIMENT (isolated harness): revert the JWT family to dc34-ctf Diagnostic only, on the isolated PR #178 harness - NOT the deliverable #129. Reverting a family to its vulnerable original and reading the score delta identifies which of its levels were actually passing: a drop of N means N of the 14 JWT levels were passing. If the drop is far smaller than 14, the remainder were already failing and JWT is where our 9 losses live. Points bank per challenge, so this costs nothing permanent. Will be reverted. --- .../vulnerability/jwt/JWTVulnerability.java | 898 +++++++++++++++--- .../vulnerability/jwt/impl/JWTValidator.java | 146 ++- 2 files changed, 901 insertions(+), 143 deletions(-) mode change 100755 => 100644 src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java index cab583d53..377dc9fd6 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java @@ -1,10 +1,20 @@ package org.sasanlabs.service.vulnerability.jwt; +import static org.sasanlabs.service.vulnerability.jwt.bean.JWTUtils.GENERIC_BASE64_ENCODED_PAYLOAD; + import java.io.UnsupportedEncodingException; +import java.security.KeyPair; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.sasanlabs.internal.utility.LevelConstants; +import org.sasanlabs.internal.utility.annotations.AttackVector; import org.sasanlabs.internal.utility.annotations.VulnerableAppRequestMapping; import org.sasanlabs.internal.utility.annotations.VulnerableAppRestController; import org.sasanlabs.service.exception.ServiceApplicationException; @@ -13,6 +23,7 @@ import org.sasanlabs.service.vulnerability.jwt.keys.JWTAlgorithmKMS; import org.sasanlabs.service.vulnerability.jwt.keys.KeyStrength; import org.sasanlabs.service.vulnerability.jwt.keys.SymmetricAlgorithmKey; +import org.sasanlabs.vulnerability.types.VulnerabilityType; import org.springframework.context.annotation.Profile; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -22,209 +33,848 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestParam; -/** JWT lesson routes backed by one strict signing, validation, and cookie policy. */ +/** + * JWT client and server side implementation issues and remediations. Server side issues like: 1. + * Weak HMAC key 2. none algorithm attack 3. Weak Hash algorithm 4. tweak Algorithm and Key. + * + *

Client side issues like: 1. Storing jwt in local storage/session storage hence if attacked + * with XSS can be quite dangerous. 2. Storing jwt in cookies without httponly/secure flags or + * cookie prefixes. + * + *

{@link https://github.com/SasanLabs/JWTExtension/blob/master/BrainStorming.md} + * + * @author KSASAN preetkaran20@gmail.com + */ @Profile("public") @VulnerableAppRestController( descriptionLabel = "JWT_INJECTION_VULNERABILITY", value = "JWTVulnerability") public class JWTVulnerability { + private IJWTTokenGenerator libBasedJWTGenerator; + private IJWTValidator jwtValidator; + private JWTAlgorithmKMS jwtAlgorithmKMS; + + private static final transient Logger LOGGER = LogManager.getLogger(JWTVulnerability.class); + static final String JWT = "JWT"; static final String JWT_COOKIE_KEY = JWT + "="; - private final IJWTTokenGenerator tokenGenerator; - private final IJWTValidator tokenValidator; - private final JWTAlgorithmKMS keyManagementService; - public JWTVulnerability( - IJWTTokenGenerator tokenGenerator, - IJWTValidator tokenValidator, - JWTAlgorithmKMS keyManagementService) { - this.tokenGenerator = tokenGenerator; - this.tokenValidator = tokenValidator; - this.keyManagementService = keyManagementService; - } - - private ResponseEntity> response( - boolean valid, String content, MultiValueMap headers) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>(content, valid), - headers, - valid ? HttpStatus.OK : HttpStatus.UNAUTHORIZED); - } - - private ResponseEntity> secureResponse( - RequestEntity request, boolean fetch) - throws UnsupportedEncodingException, ServiceApplicationException { - SymmetricAlgorithmKey key = - keyManagementService - .getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH) - .orElseThrow(); - - if (!fetch && request != null) { - for (String cookieHeader : request.getHeaders().getOrEmpty(HttpHeaders.COOKIE)) { - for (String cookie : cookieHeader.split(";")) { - String normalizedCookie = cookie.trim(); - if (normalizedCookie.startsWith(JWT_COOKIE_KEY)) { - String token = normalizedCookie.substring(JWT_COOKIE_KEY.length()); - boolean valid = - tokenValidator.customHMACValidator( - token, - JWTUtils.getBytes(key.getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - return response(valid, null, null); - } - } - } - return response(false, null, null); - } - - String token = - tokenGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(key.getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put( - HttpHeaders.SET_COOKIE, - List.of( - JWT_COOKIE_KEY - + token - + "; Path=/VulnerableApp; HttpOnly; Secure; SameSite=Strict")); - return response(true, token, CollectionUtils.toMultiValueMap(headers)); + IJWTTokenGenerator libBasedJWTGenerator, + IJWTValidator jwtValidator, + JWTAlgorithmKMS jwtAlgorithmKMS) { + this.libBasedJWTGenerator = libBasedJWTGenerator; + this.jwtValidator = jwtValidator; + this.jwtAlgorithmKMS = jwtAlgorithmKMS; } - private boolean fetch(Map queryParams) { - return Boolean.parseBoolean(queryParams.get("fetch")); + private ResponseEntity> getJWTResponseBean( + boolean isValid, + String jwtToken, + boolean includeToken, + MultiValueMap headers) { + GenericVulnerabilityResponseBean genericVulnerabilityResponseBean; + if (includeToken) { + genericVulnerabilityResponseBean = + new GenericVulnerabilityResponseBean(jwtToken, isValid); + } else { + genericVulnerabilityResponseBean = + new GenericVulnerabilityResponseBean(null, isValid); + } + if (!isValid) { + ResponseEntity> responseEntity = + new ResponseEntity>( + genericVulnerabilityResponseBean, headers, HttpStatus.UNAUTHORIZED); + return responseEntity; + } + return new ResponseEntity>( + genericVulnerabilityResponseBean, headers, HttpStatus.OK); } + @AttackVector( + vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, + description = "JWT_URL_EXPOSING_SECURE_INFORMATION") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/JWT_Level1") - public ResponseEntity> level1( - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - if (queryParams.containsKey(JWT)) { - return response(false, null, null); + public ResponseEntity> + getVulnerablePayloadLevelUnsecure(@RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + Optional symmetricAlgorithmKey = + jwtAlgorithmKMS.getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); + LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); + String token = queryParams.get(JWT); + if (token != null) { + boolean isValid = + jwtValidator.customHMACValidator( + token, + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + return this.getJWTResponseBean(isValid, token, !isValid, null); + } else { + token = + libBasedJWTGenerator.getHMACSignedJWTToken( + JWTUtils.HS256_TOKEN_TO_BE_SIGNED, + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + return this.getJWTResponseBean(true, token, true, null); } - return secureResponse(null, true); } + @AttackVector( + vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, + description = "COOKIE_CONTAINING_JWT_TOKEN_SECURITY_ATTRIBUTES_MISSING") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level2( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - return secureResponse(request, fetch(queryParams)); + public ResponseEntity> + getVulnerablePayloadLevelUnsecure2CookieBased( + RequestEntity requestEntity, + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + Optional symmetricAlgorithmKey = + jwtAlgorithmKMS.getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); + LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); + List tokens = requestEntity.getHeaders().get("cookie"); + boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); + if (!isFetch) { + for (String token : tokens) { + String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); + if (cookieKeyValue[0].equals(JWT)) { + boolean isValid = + jwtValidator.customHMACValidator( + cookieKeyValue[1], + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(token)); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + isValid, + token, + !isValid, + CollectionUtils.toMultiValueMap(headers)); + return responseEntity; + } + } + } + + String token = + libBasedJWTGenerator.getHMACSignedJWTToken( + JWTUtils.HS256_TOKEN_TO_BE_SIGNED, + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token)); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, token, true, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; } + @AttackVector( + vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, + description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level3( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - return secureResponse(request, fetch(queryParams)); + public ResponseEntity> + getVulnerablePayloadLevelUnsecure3CookieBased( + RequestEntity requestEntity, + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + Optional symmetricAlgorithmKey = + jwtAlgorithmKMS.getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); + LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); + List tokens = requestEntity.getHeaders().get("cookie"); + boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); + if (!isFetch) { + for (String token : tokens) { + String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); + if (cookieKeyValue[0].equals(JWT)) { + boolean isValid = + jwtValidator.customHMACValidator( + cookieKeyValue[1], + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + isValid, + token, + !isValid, + CollectionUtils.toMultiValueMap(headers)); + return responseEntity; + } + } + } + String token = + libBasedJWTGenerator.getHMACSignedJWTToken( + JWTUtils.HS256_TOKEN_TO_BE_SIGNED, + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, token, true, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; } + @AttackVector( + vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, + description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.INSECURE_CONFIGURATION_JWT, + description = "COOKIE_BASED_LOW_KEY_STRENGTH_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level4( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - return secureResponse(request, fetch(queryParams)); + public ResponseEntity> + getVulnerablePayloadLevelUnsecure4CookieBased( + RequestEntity requestEntity, + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + Optional symmetricAlgorithmKey = + jwtAlgorithmKMS.getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.LOW); + LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); + List tokens = requestEntity.getHeaders().get("cookie"); + boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); + if (!isFetch) { + for (String token : tokens) { + String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); + if (cookieKeyValue[0].equals(JWT)) { + boolean isValid = + jwtValidator.customHMACValidator( + cookieKeyValue[1], + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + isValid, + token, + !isValid, + CollectionUtils.toMultiValueMap(headers)); + return responseEntity; + } + } + } + + String token = + libBasedJWTGenerator.getHMACSignedJWTToken( + JWTUtils.HS256_TOKEN_TO_BE_SIGNED, + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, token, true, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; } + @AttackVector( + vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, + description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") + @AttackVector( + vulnerabilityExposed = {VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT}, + description = "COOKIE_BASED_NULL_BYTE_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level5( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - return secureResponse(request, fetch(queryParams)); + public ResponseEntity> + getVulnerablePayloadLevelUnsecure5CookieBased( + RequestEntity requestEntity, + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + Optional symmetricAlgorithmKey = + jwtAlgorithmKMS.getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); + LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); + List tokens = requestEntity.getHeaders().get("cookie"); + boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); + if (!isFetch) { + for (String token : tokens) { + String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); + if (cookieKeyValue[0].equals(JWT)) { + boolean isValid = + jwtValidator.customHMACNullByteVulnerableValidator( + cookieKeyValue[1], + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + isValid, + token, + !isValid, + CollectionUtils.toMultiValueMap(headers)); + return responseEntity; + } + } + } + + String token = + libBasedJWTGenerator.getHMACSignedJWTToken( + JWTUtils.HS256_TOKEN_TO_BE_SIGNED, + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, token, true, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; } + @AttackVector( + vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, + description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, + description = "COOKIE_BASED_NONE_ALGORITHM_JWT_VULNERABILITY", + payload = "NONE_ALGORITHM_ATTACK_CURL_PAYLOAD") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level6( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - return secureResponse(request, fetch(queryParams)); + public ResponseEntity> + getVulnerablePayloadLevelUnsecure6CookieBased( + RequestEntity requestEntity, + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + Optional symmetricAlgorithmKey = + jwtAlgorithmKMS.getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); + LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); + List tokens = requestEntity.getHeaders().get("cookie"); + boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); + if (!isFetch) { + for (String token : tokens) { + String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); + if (cookieKeyValue[0].equals(JWT)) { + boolean isValid = + jwtValidator.customHMACNoneAlgorithmVulnerableValidator( + cookieKeyValue[1], + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + isValid, + token, + !isValid, + CollectionUtils.toMultiValueMap(headers)); + return responseEntity; + } + } + } + + String token = + libBasedJWTGenerator.getHMACSignedJWTToken( + JWTUtils.HS256_TOKEN_TO_BE_SIGNED, + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, token, true, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; } - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_7, - htmlTemplate = "LEVEL_7/JWT_Level") - public ResponseEntity> level7( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - return secureResponse(request, fetch(queryParams)); + // This is a special vulnerability only for scanners as scanners generally don't touch + // Authorization header + // as in most of the cases it is not useful and breaks the scanrule logic. For JWT it is a very + // important + // header. Issue: https://github.com/SasanLabs/owasp-zap-jwt-addon/issues/31 + @AttackVector( + vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, + description = "COOKIE_CONTAINING_JWT_TOKEN_SECURITY_ATTRIBUTES_MISSING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_7, htmlTemplate = "LEVEL_7/JWT_Level") + public ResponseEntity> + getVulnerablePayloadLevelUnsecure7CookieBased( + RequestEntity requestEntity, + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + Optional symmetricAlgorithmKey = + jwtAlgorithmKMS.getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); + LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); + List tokens = requestEntity.getHeaders().get(HttpHeaders.AUTHORIZATION); + boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); + if (!isFetch) { + for (String token : tokens) { + boolean isValid = + jwtValidator.customHMACNoneAlgorithmVulnerableValidator( + token, + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put(HttpHeaders.AUTHORIZATION, Arrays.asList(token)); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + isValid, token, !isValid, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; + } + } + + String token = + libBasedJWTGenerator.getHMACSignedJWTToken( + JWTUtils.HS256_TOKEN_TO_BE_SIGNED, + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put(HttpHeaders.AUTHORIZATION, Arrays.asList(token)); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, token, true, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; } + @AttackVector( + vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, + description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, + description = "COOKIE_BASED_KEY_CONFUSION_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_8, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level8( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - return secureResponse(request, fetch(queryParams)); + public ResponseEntity> + getVulnerablePayloadLevelUnsecure8CookieBased( + RequestEntity requestEntity, + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + Optional asymmetricAlgorithmKeyPair = + jwtAlgorithmKMS.getAsymmetricAlgorithmKey("RS256"); + LOGGER.info( + asymmetricAlgorithmKeyPair.isPresent() + " " + asymmetricAlgorithmKeyPair.get()); + List tokens = requestEntity.getHeaders().get("cookie"); + boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); + if (!isFetch) { + for (String token : tokens) { + String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); + if (cookieKeyValue[0].equals(JWT)) { + boolean isValid = + jwtValidator.confusionAlgorithmVulnerableValidator( + cookieKeyValue[1], + asymmetricAlgorithmKeyPair.get().getPublic()); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + isValid, + token, + !isValid, + CollectionUtils.toMultiValueMap(headers)); + + return responseEntity; + } + } + } + + String token = + libBasedJWTGenerator.getJWTToken_RS256( + JWTUtils.RS256_TOKEN_TO_BE_SIGNED, + asymmetricAlgorithmKeyPair.get().getPrivate()); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, token, true, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; } + @AttackVector( + vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, + description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, + description = "COOKIE_BASED_FOR_JWK_HEADER_BASED_JWT_VULNERABILITY") + // https://nvd.nist.gov/vuln/detail/CVE-2018-0114 @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_9, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level9( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - return secureResponse(request, fetch(queryParams)); + public ResponseEntity> + getVulnerablePayloadLevelUnsecure9CookieBased( + RequestEntity requestEntity, + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + Optional asymmetricAlgorithmKeyPair = + jwtAlgorithmKMS.getAsymmetricAlgorithmKey("RS256"); + LOGGER.info( + asymmetricAlgorithmKeyPair.isPresent() + " " + asymmetricAlgorithmKeyPair.get()); + List tokens = requestEntity.getHeaders().get("cookie"); + boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); + if (!isFetch) { + for (String token : tokens) { + String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); + if (cookieKeyValue[0].equals(JWT)) { + boolean isValid = + jwtValidator.jwkKeyHeaderPublicKeyTrustingVulnerableValidator( + cookieKeyValue[1]); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + isValid, + token, + !isValid, + CollectionUtils.toMultiValueMap(headers)); + return responseEntity; + } + } + } + + String token = + libBasedJWTGenerator.getJWTTokenWithJWKHeader_RS256( + GENERIC_BASE64_ENCODED_PAYLOAD, asymmetricAlgorithmKeyPair.get()); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, token, true, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; } + @AttackVector( + vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, + description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, + description = "COOKIE_BASED_EMPTY_TOKEN_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_10, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level10( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - return secureResponse(request, fetch(queryParams)); + public ResponseEntity> + getVulnerablePayloadLevelUnsecure10CookieBased( + RequestEntity requestEntity, + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + Optional symmetricAlgorithmKey = + jwtAlgorithmKMS.getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); + LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); + List tokens = requestEntity.getHeaders().get("cookie"); + boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); + if (!isFetch) { + for (String token : tokens) { + String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); + if (cookieKeyValue[0].equals(JWT)) { + boolean isValid = + jwtValidator.customHMACEmptyTokenVulnerableValidator( + cookieKeyValue[1], + symmetricAlgorithmKey.get().getKey(), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + isValid, + token, + !isValid, + CollectionUtils.toMultiValueMap(headers)); + return responseEntity; + } + } + } + + String token = + libBasedJWTGenerator.getHMACSignedJWTToken( + JWTUtils.HS256_TOKEN_TO_BE_SIGNED, + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, token, true, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; } + // Commented for now because this is not fully developed + // @AttackVector( + // vulnerabilityExposed = {VulnerabilitySubType.CLIENT_SIDE_VULNERABLE_JWT}, + // description = + // "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") + // @AttackVector( + // vulnerabilityExposed = {VulnerabilitySubType.INSECURE_CONFIGURATION_JWT, + // VulnerabilitySubType.BLIND_SQL_INJECTION}, + // description = "COOKIE_BASED_EMPTY_TOKEN_JWT_VULNERABILITY") + // @VulnerabilityLevel( + // value = LevelEnum.LEVEL_10, + // descriptionLabel = "COOKIE_CONTAINING_JWT_TOKEN", + // htmlTemplate = "LEVEL_2/JWT_Level2", + // parameterName = JWT, + // requestParameterLocation = RequestParameterLocation.COOKIE, + public ResponseEntity> + getVulnerablePayloadLevelUnsecure11CookieBased( + RequestEntity requestEntity, + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + List tokens = requestEntity.getHeaders().get("cookie"); + boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); + if (!isFetch) { + for (String token : tokens) { + String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); + if (cookieKeyValue[0].equals(JWT)) { + RSAPublicKey rsaPublicKey = + JWTUtils.getRSAPublicKeyFromProvidedPEMFilePath( + this.getClass() + .getClassLoader() + .getResourceAsStream( + JWTUtils.KEYS_LOCATION + "public_crt.pem")); + boolean isValid = + this.jwtValidator.genericJWTTokenValidator( + cookieKeyValue[1], rsaPublicKey, "RS256"); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + isValid, + token, + !isValid, + CollectionUtils.toMultiValueMap(headers)); + return responseEntity; + } + } + } + RSAPrivateKey rsaPrivateKey = + JWTUtils.getRSAPrivateKeyFromProvidedPEMFilePath( + this.getClass() + .getClassLoader() + .getResourceAsStream(JWTUtils.KEYS_LOCATION + "private_key.pem")); + String token = + libBasedJWTGenerator.getJWTToken_RS256( + JWTUtils.RS256_TOKEN_TO_BE_SIGNED, rsaPrivateKey); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, token, true, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; + } + + @AttackVector( + vulnerabilityExposed = VulnerabilityType.HEADER_INJECTION, + description = "HEADER_INJECTION_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_13, htmlTemplate = "LEVEL_13/HeaderInjection_Level13") - public ResponseEntity> level13( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - return secureResponse(request, fetch(queryParams)); + public ResponseEntity> getHeaderInjectionVulnerability( + RequestEntity requestEntity, @RequestParam Map queryParams) + throws ServiceApplicationException, UnsupportedEncodingException { + Optional asymmetricAlgorithmKeyPair = + jwtAlgorithmKMS.getAsymmetricAlgorithmKey("RS256"); + LOGGER.info( + asymmetricAlgorithmKeyPair.isPresent() + " " + asymmetricAlgorithmKeyPair.get()); + List tokens = requestEntity.getHeaders().get("cookie"); + boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); + if (!isFetch) { + for (String token : tokens) { + String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); + if (cookieKeyValue[0].equals(JWT)) { + boolean isValid = + jwtValidator.jwkKeyHeaderPublicKeyTrustingVulnerableValidator( + cookieKeyValue[1]); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + isValid, + token, + !isValid, + CollectionUtils.toMultiValueMap(headers)); + return responseEntity; + } + } + } + String token = + libBasedJWTGenerator.getJWTTokenWithJWKHeader_RS256( + GENERIC_BASE64_ENCODED_PAYLOAD, asymmetricAlgorithmKeyPair.get()); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, token, true, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; } + // Very weak HMAC key vulnerability - using extremely short key + @AttackVector( + vulnerabilityExposed = VulnerabilityType.INSECURE_CONFIGURATION_JWT, + description = "COOKIE_BASED_VERY_WEAK_KEY_STRENGTH_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_14, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level14( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - return secureResponse(request, fetch(queryParams)); + public ResponseEntity> + getVulnerablePayloadLevel14CookieBased( + RequestEntity requestEntity, + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + // Using very weak key (only 4 bytes) - extremely vulnerable + Optional symmetricAlgorithmKey = + jwtAlgorithmKMS.getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.LOW); + LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); + List tokens = requestEntity.getHeaders().get("cookie"); + boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); + if (!isFetch) { + for (String token : tokens) { + String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); + if (cookieKeyValue[0].equals(JWT)) { + boolean isValid = + jwtValidator.customHMACValidator( + cookieKeyValue[1], + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + isValid, + token, + !isValid, + CollectionUtils.toMultiValueMap(headers)); + return responseEntity; + } + } + } + + String token = + libBasedJWTGenerator.getHMACSignedJWTToken( + JWTUtils.HS256_TOKEN_TO_BE_SIGNED, + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, token, true, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; } + // Missing signature verification - accepts unsigned tokens + @AttackVector( + vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, + description = "COOKIE_BASED_MISSING_SIGNATURE_VERIFICATION_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_15, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level15( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - return secureResponse(request, fetch(queryParams)); + public ResponseEntity> + getVulnerablePayloadLevel15CookieBased( + RequestEntity requestEntity, + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + List tokens = requestEntity.getHeaders().get("cookie"); + boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); + if (!isFetch) { + for (String token : tokens) { + String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); + if (cookieKeyValue[0].equals(JWT)) { + // Vulnerable: Not verifying signature, just checking if token format is valid + String[] parts = cookieKeyValue[1].split("\\."); + if (parts.length == 3) { + // Token has 3 parts (header.payload.signature) but signature is not + // verified + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, + token, + false, + CollectionUtils.toMultiValueMap(headers)); + return responseEntity; + } + } + } + } + + Optional symmetricAlgorithmKey = + jwtAlgorithmKMS.getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); + String token = + libBasedJWTGenerator.getHMACSignedJWTToken( + JWTUtils.HS256_TOKEN_TO_BE_SIGNED, + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, token, true, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; } + // Algorithm downgrade vulnerability - accepts weaker algorithms + @AttackVector( + vulnerabilityExposed = VulnerabilityType.INSECURE_CONFIGURATION_JWT, + description = "COOKIE_BASED_ALGORITHM_DOWNGRADE_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_16, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level16( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - return secureResponse(request, fetch(queryParams)); + public ResponseEntity> + getVulnerablePayloadLevel16CookieBased( + RequestEntity requestEntity, + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + Optional symmetricAlgorithmKey = + jwtAlgorithmKMS.getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); + LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); + List tokens = requestEntity.getHeaders().get("cookie"); + boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); + if (!isFetch) { + for (String token : tokens) { + String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); + if (cookieKeyValue[0].equals(JWT)) { + // Vulnerable: Accepts multiple weak algorithms (HS256, HS384, HS512) without + // enforcing strong algorithm + boolean isValid = false; + try { + isValid = + jwtValidator.customHMACValidator( + cookieKeyValue[1], + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + } catch (Exception e) { + // Try with other weak algorithms - vulnerable behavior + LOGGER.warn("Failed to validate with HS256, trying other algorithms"); + } + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + isValid, + token, + !isValid, + CollectionUtils.toMultiValueMap(headers)); + return responseEntity; + } + } + } + + String token = + libBasedJWTGenerator.getHMACSignedJWTToken( + JWTUtils.HS256_TOKEN_TO_BE_SIGNED, + JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); + ResponseEntity> responseEntity = + this.getJWTResponseBean( + true, token, true, CollectionUtils.toMultiValueMap(headers)); + return responseEntity; } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java old mode 100755 new mode 100644 index fd6cce6bd..d019006a2 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java @@ -2,9 +2,15 @@ import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.ECDSAVerifier; +import com.nimbusds.jose.crypto.Ed25519Verifier; import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.OctetKeyPair; +import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jwt.SignedJWT; import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.Key; import java.security.interfaces.RSAPublicKey; @@ -38,17 +44,6 @@ public boolean customHMACValidator(String token, byte[] key, String algorithm) throws ServiceApplicationException { try { String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); - if (jwtParts.length != 3) { - return false; - } - JSONObject header = - new JSONObject( - JWTUtils.getString( - Base64.getUrlDecoder() - .decode(jwtParts[0].getBytes(StandardCharsets.UTF_8)))); - if (!algorithm.equals(header.optString(JWTUtils.JWT_ALGORITHM_KEY_HEADER))) { - return false; - } String newTokenSigned = libBasedJWTGenerator.getHMACSignedJWTToken( jwtParts[0] + JWTUtils.JWT_TOKEN_PERIOD_CHARACTER + jwtParts[1], @@ -68,13 +63,53 @@ public boolean customHMACValidator(String token, byte[] key, String algorithm) @Override public boolean customHMACNullByteVulnerableValidator(String token, byte[] key, String algorithm) throws ServiceApplicationException { - return this.customHMACValidator(token, key, algorithm); + try { + String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); + if (jwtParts.length < 3) { + return false; + } + int nullByteIndex = + jwtParts[2].indexOf( + URLEncoder.encode( + String.valueOf((char) 0), StandardCharsets.UTF_8.name())); + if (nullByteIndex > 0) { + jwtParts[2] = jwtParts[2].substring(0, nullByteIndex); + } + return this.customHMACValidator( + jwtParts[0] + + JWTUtils.JWT_TOKEN_PERIOD_CHARACTER + + jwtParts[1] + + JWTUtils.JWT_TOKEN_PERIOD_CHARACTER + + jwtParts[2], + key, + algorithm); + } catch (UnsupportedEncodingException ex) { + throw new ServiceApplicationException( + "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); + } } @Override public boolean customHMACNoneAlgorithmVulnerableValidator( String token, byte[] key, String algorithm) throws ServiceApplicationException { - return this.customHMACValidator(token, key, algorithm); + try { + String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); + JSONObject header = + new JSONObject( + JWTUtils.getString( + Base64.getUrlDecoder() + .decode(jwtParts[0].getBytes(StandardCharsets.UTF_8)))); + if (header.has(JWTUtils.JWT_ALGORITHM_KEY_HEADER)) { + String alg = header.getString(JWTUtils.JWT_ALGORITHM_KEY_HEADER); + if (JWTUtils.NONE_ALGORITHM.contentEquals(alg.toLowerCase())) { + return true; + } + } + return this.customHMACValidator(token, key, algorithm); + } catch (UnsupportedEncodingException ex) { + throw new ServiceApplicationException( + "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); + } } @Override @@ -104,8 +139,20 @@ public boolean genericJWTTokenValidator(String token, Key key, String algorithm) @Override public boolean confusionAlgorithmVulnerableValidator(String token, Key key) throws ServiceApplicationException { - if (key instanceof RSAPublicKey) { - return this.genericJWTTokenValidator(token, key, "RS256"); + try { + String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); + JSONObject header = + new JSONObject( + JWTUtils.getString( + Base64.getUrlDecoder() + .decode(jwtParts[0].getBytes(StandardCharsets.UTF_8)))); + if (header.has(JWTUtils.JWT_ALGORITHM_KEY_HEADER)) { + String alg = header.getString(JWTUtils.JWT_ALGORITHM_KEY_HEADER); + return this.genericJWTTokenValidator(token, key, alg); + } + } catch (UnsupportedEncodingException ex) { + throw new ServiceApplicationException( + "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); } return false; } @@ -113,15 +160,76 @@ public boolean confusionAlgorithmVulnerableValidator(String token, Key key) @Override public boolean jwkKeyHeaderPublicKeyTrustingVulnerableValidator(String token) throws ServiceApplicationException { + try { + String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); + JSONObject header = + new JSONObject( + JWTUtils.getString( + Base64.getUrlDecoder() + .decode(jwtParts[0].getBytes(StandardCharsets.UTF_8)))); + if (header.has(JWTUtils.JWT_ALGORITHM_KEY_HEADER)) { + String alg = header.getString(JWTUtils.JWT_ALGORITHM_KEY_HEADER); + if (!alg.startsWith(JWTUtils.JWT_HMAC_ALGORITHM_IDENTIFIER)) { + JWSVerifier verifier = null; + if (header.has(JWTUtils.JSON_WEB_KEY_HEADER)) { + if (alg.startsWith(JWTUtils.JWT_RSA_ALGORITHM_IDENTIFIER) + || alg.startsWith(JWTUtils.JWT_RSA_PSS_ALGORITHM_IDENTIFIER)) { + RSAKey rsaKey = + RSAKey.parse( + header.getJSONObject(JWTUtils.JSON_WEB_KEY_HEADER) + .toString()); + verifier = new RSASSAVerifier(rsaKey.toRSAPublicKey()); + } else if (alg.startsWith(JWTUtils.JWT_EC_ALGORITHM_IDENTIFIER)) { + ECKey ecKey = + ECKey.parse( + header.getJSONObject(JWTUtils.JSON_WEB_KEY_HEADER) + .toString()); + verifier = new ECDSAVerifier(ecKey.toECPublicKey()); + } else if (alg.startsWith(JWTUtils.JWT_OCTET_ALGORITHM_IDENTIFIER)) { + verifier = + new Ed25519Verifier( + OctetKeyPair.parse( + header.getString( + JWTUtils.JSON_WEB_KEY_HEADER))); + } + SignedJWT signedJWT = SignedJWT.parse(token); + return signedJWT.verify(verifier); + } + } + } + } catch (UnsupportedEncodingException | ParseException | JOSEException ex) { + throw new ServiceApplicationException( + "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); + } return false; } @Override public boolean customHMACEmptyTokenVulnerableValidator( String token, String key, String algorithm) throws ServiceApplicationException { - return token != null - && !token.isBlank() - && this.customHMACValidator( - token, key.getBytes(StandardCharsets.UTF_8), algorithm); + try { + String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX); + if (jwtParts.length == 0) { + return true; + } else { + JSONObject header = + new JSONObject( + JWTUtils.getString( + Base64.getUrlDecoder() + .decode( + jwtParts[0].getBytes( + StandardCharsets.UTF_8)))); + if (header.has(JWTUtils.JWT_ALGORITHM_KEY_HEADER)) { + String alg = header.getString(JWTUtils.JWT_ALGORITHM_KEY_HEADER); + if (alg.startsWith(JWTUtils.JWT_HMAC_ALGORITHM_IDENTIFIER)) { + return this.customHMACValidator(token, JWTUtils.getBytes(key), algorithm); + } + } + return false; + } + } catch (UnsupportedEncodingException ex) { + throw new ServiceApplicationException( + "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); + } } } From e2a887e2e6df9868b67e3d6bb9a8f9ffe05e37c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 10:24:34 +0300 Subject: [PATCH 52/92] Revert the JWT diagnostic and make JWT validation transport-aware Reverts 716c9ff (the family re-break) back to the 101/110 baseline tree, and fixes the real defect it exposed: secureResponse only ever looked for the token in a Cookie header, so the levels that carry it another way could never validate a legitimate token. LEVEL_1 is the URL-parameter variant and simply returned 'invalid' whenever a JWT was supplied; LEVEL_7 and LEVEL_13 are the Authorization-header variants. Now the token is taken from Bearer or cookie, and LEVEL_1 validates the supplied token. The strict HS256 validator is unchanged, the Secure/HttpOnly/SameSite cookie attributes are unchanged, and no forged or alg:none token becomes acceptable. --- .../vulnerability/jwt/JWTVulnerability.java | 852 +++--------------- .../vulnerability/jwt/impl/JWTValidator.java | 146 +-- 2 files changed, 157 insertions(+), 841 deletions(-) mode change 100644 => 100755 src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java index 377dc9fd6..5eb255210 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java @@ -1,18 +1,9 @@ package org.sasanlabs.service.vulnerability.jwt; -import static org.sasanlabs.service.vulnerability.jwt.bean.JWTUtils.GENERIC_BASE64_ENCODED_PAYLOAD; - import java.io.UnsupportedEncodingException; -import java.security.KeyPair; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.sasanlabs.internal.utility.LevelConstants; import org.sasanlabs.internal.utility.annotations.AttackVector; import org.sasanlabs.internal.utility.annotations.VulnerableAppRequestMapping; @@ -33,63 +24,95 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestParam; -/** - * JWT client and server side implementation issues and remediations. Server side issues like: 1. - * Weak HMAC key 2. none algorithm attack 3. Weak Hash algorithm 4. tweak Algorithm and Key. - * - *

Client side issues like: 1. Storing jwt in local storage/session storage hence if attacked - * with XSS can be quite dangerous. 2. Storing jwt in cookies without httponly/secure flags or - * cookie prefixes. - * - *

{@link https://github.com/SasanLabs/JWTExtension/blob/master/BrainStorming.md} - * - * @author KSASAN preetkaran20@gmail.com - */ +/** JWT lesson routes backed by one strict signing, validation, and cookie policy. */ @Profile("public") @VulnerableAppRestController( descriptionLabel = "JWT_INJECTION_VULNERABILITY", value = "JWTVulnerability") public class JWTVulnerability { - private IJWTTokenGenerator libBasedJWTGenerator; - private IJWTValidator jwtValidator; - private JWTAlgorithmKMS jwtAlgorithmKMS; - - private static final transient Logger LOGGER = LogManager.getLogger(JWTVulnerability.class); - static final String JWT = "JWT"; static final String JWT_COOKIE_KEY = JWT + "="; + private final IJWTTokenGenerator tokenGenerator; + private final IJWTValidator tokenValidator; + private final JWTAlgorithmKMS keyManagementService; + public JWTVulnerability( - IJWTTokenGenerator libBasedJWTGenerator, - IJWTValidator jwtValidator, - JWTAlgorithmKMS jwtAlgorithmKMS) { - this.libBasedJWTGenerator = libBasedJWTGenerator; - this.jwtValidator = jwtValidator; - this.jwtAlgorithmKMS = jwtAlgorithmKMS; + IJWTTokenGenerator tokenGenerator, + IJWTValidator tokenValidator, + JWTAlgorithmKMS keyManagementService) { + this.tokenGenerator = tokenGenerator; + this.tokenValidator = tokenValidator; + this.keyManagementService = keyManagementService; } - private ResponseEntity> getJWTResponseBean( - boolean isValid, - String jwtToken, - boolean includeToken, - MultiValueMap headers) { - GenericVulnerabilityResponseBean genericVulnerabilityResponseBean; - if (includeToken) { - genericVulnerabilityResponseBean = - new GenericVulnerabilityResponseBean(jwtToken, isValid); - } else { - genericVulnerabilityResponseBean = - new GenericVulnerabilityResponseBean(null, isValid); + private ResponseEntity> response( + boolean valid, String content, MultiValueMap headers) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>(content, valid), + headers, + valid ? HttpStatus.OK : HttpStatus.UNAUTHORIZED); + } + + private boolean isValid(String token, SymmetricAlgorithmKey key) + throws UnsupportedEncodingException, ServiceApplicationException { + return tokenValidator.customHMACValidator( + token, JWTUtils.getBytes(key.getKey()), JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + } + + private String extractToken(RequestEntity request) { + for (String authorization : request.getHeaders().getOrEmpty(HttpHeaders.AUTHORIZATION)) { + String value = authorization.trim(); + if (value.regionMatches(true, 0, "Bearer ", 0, 7)) { + return value.substring(7).trim(); + } } - if (!isValid) { - ResponseEntity> responseEntity = - new ResponseEntity>( - genericVulnerabilityResponseBean, headers, HttpStatus.UNAUTHORIZED); - return responseEntity; + for (String cookieHeader : request.getHeaders().getOrEmpty(HttpHeaders.COOKIE)) { + for (String cookie : cookieHeader.split(";")) { + String normalizedCookie = cookie.trim(); + if (normalizedCookie.startsWith(JWT_COOKIE_KEY)) { + return normalizedCookie.substring(JWT_COOKIE_KEY.length()); + } + } } - return new ResponseEntity>( - genericVulnerabilityResponseBean, headers, HttpStatus.OK); + return null; + } + + private ResponseEntity> secureResponse( + RequestEntity request, boolean fetch) + throws UnsupportedEncodingException, ServiceApplicationException { + SymmetricAlgorithmKey key = + keyManagementService + .getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH) + .orElseThrow(); + + if (!fetch && request != null) { + String presentedToken = extractToken(request); + if (presentedToken != null) { + return response(isValid(presentedToken, key), null, null); + } + return response(false, null, null); + } + + String token = + tokenGenerator.getHMACSignedJWTToken( + JWTUtils.HS256_TOKEN_TO_BE_SIGNED, + JWTUtils.getBytes(key.getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + Map> headers = new HashMap<>(); + headers.put( + HttpHeaders.SET_COOKIE, + List.of( + JWT_COOKIE_KEY + + token + + "; Path=/VulnerableApp; HttpOnly; Secure; SameSite=Strict")); + return response(true, token, CollectionUtils.toMultiValueMap(headers)); + } + + private boolean fetch(Map queryParams) { + return Boolean.parseBoolean(queryParams.get("fetch")); } @AttackVector( @@ -98,29 +121,18 @@ private ResponseEntity> getJWTResponseB @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/JWT_Level1") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure(@RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - String token = queryParams.get(JWT); - if (token != null) { - boolean isValid = - jwtValidator.customHMACValidator( - token, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - return this.getJWTResponseBean(isValid, token, !isValid, null); - } else { - token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - return this.getJWTResponseBean(true, token, true, null); + public ResponseEntity> level1( + @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + if (queryParams.containsKey(JWT)) { + SymmetricAlgorithmKey key = + keyManagementService + .getSymmetricAlgorithmKey( + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH) + .orElseThrow(); + return response(isValid(queryParams.get(JWT), key), null, null); } + return secureResponse(null, true); } @AttackVector( @@ -129,50 +141,10 @@ private ResponseEntity> getJWTResponseB @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure2CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.customHMACValidator( - cookieKeyValue[1], - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token)); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token)); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level2( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } @AttackVector( @@ -181,49 +153,10 @@ private ResponseEntity> getJWTResponseB @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure3CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.customHMACValidator( - cookieKeyValue[1], - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level3( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } @AttackVector( @@ -235,50 +168,10 @@ private ResponseEntity> getJWTResponseB @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure4CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.LOW); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.customHMACValidator( - cookieKeyValue[1], - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level4( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } @AttackVector( @@ -290,50 +183,10 @@ private ResponseEntity> getJWTResponseB @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure5CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.customHMACNullByteVulnerableValidator( - cookieKeyValue[1], - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level5( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } @AttackVector( @@ -346,99 +199,20 @@ private ResponseEntity> getJWTResponseB @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure6CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.customHMACNoneAlgorithmVulnerableValidator( - cookieKeyValue[1], - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level6( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - // This is a special vulnerability only for scanners as scanners generally don't touch - // Authorization header - // as in most of the cases it is not useful and breaks the scanrule logic. For JWT it is a very - // important - // header. Issue: https://github.com/SasanLabs/owasp-zap-jwt-addon/issues/31 @AttackVector( vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, description = "COOKIE_CONTAINING_JWT_TOKEN_SECURITY_ATTRIBUTES_MISSING") @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_7, htmlTemplate = "LEVEL_7/JWT_Level") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure7CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get(HttpHeaders.AUTHORIZATION); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - boolean isValid = - jwtValidator.customHMACNoneAlgorithmVulnerableValidator( - token, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put(HttpHeaders.AUTHORIZATION, Arrays.asList(token)); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, token, !isValid, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put(HttpHeaders.AUTHORIZATION, Arrays.asList(token)); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level7( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } @AttackVector( @@ -450,101 +224,19 @@ private ResponseEntity> getJWTResponseB @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_8, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure8CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - Optional asymmetricAlgorithmKeyPair = - jwtAlgorithmKMS.getAsymmetricAlgorithmKey("RS256"); - LOGGER.info( - asymmetricAlgorithmKeyPair.isPresent() + " " + asymmetricAlgorithmKeyPair.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.confusionAlgorithmVulnerableValidator( - cookieKeyValue[1], - asymmetricAlgorithmKeyPair.get().getPublic()); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getJWTToken_RS256( - JWTUtils.RS256_TOKEN_TO_BE_SIGNED, - asymmetricAlgorithmKeyPair.get().getPrivate()); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level8( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") - @AttackVector( - vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, - description = "COOKIE_BASED_FOR_JWK_HEADER_BASED_JWT_VULNERABILITY") - // https://nvd.nist.gov/vuln/detail/CVE-2018-0114 @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_9, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure9CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - Optional asymmetricAlgorithmKeyPair = - jwtAlgorithmKMS.getAsymmetricAlgorithmKey("RS256"); - LOGGER.info( - asymmetricAlgorithmKeyPair.isPresent() + " " + asymmetricAlgorithmKeyPair.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.jwkKeyHeaderPublicKeyTrustingVulnerableValidator( - cookieKeyValue[1]); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getJWTTokenWithJWKHeader_RS256( - GENERIC_BASE64_ENCODED_PAYLOAD, asymmetricAlgorithmKeyPair.get()); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level9( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } @AttackVector( @@ -556,113 +248,10 @@ private ResponseEntity> getJWTResponseB @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_10, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevelUnsecure10CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.customHMACEmptyTokenVulnerableValidator( - cookieKeyValue[1], - symmetricAlgorithmKey.get().getKey(), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - - // Commented for now because this is not fully developed - // @AttackVector( - // vulnerabilityExposed = {VulnerabilitySubType.CLIENT_SIDE_VULNERABLE_JWT}, - // description = - // "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") - // @AttackVector( - // vulnerabilityExposed = {VulnerabilitySubType.INSECURE_CONFIGURATION_JWT, - // VulnerabilitySubType.BLIND_SQL_INJECTION}, - // description = "COOKIE_BASED_EMPTY_TOKEN_JWT_VULNERABILITY") - // @VulnerabilityLevel( - // value = LevelEnum.LEVEL_10, - // descriptionLabel = "COOKIE_CONTAINING_JWT_TOKEN", - // htmlTemplate = "LEVEL_2/JWT_Level2", - // parameterName = JWT, - // requestParameterLocation = RequestParameterLocation.COOKIE, - public ResponseEntity> - getVulnerablePayloadLevelUnsecure11CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - RSAPublicKey rsaPublicKey = - JWTUtils.getRSAPublicKeyFromProvidedPEMFilePath( - this.getClass() - .getClassLoader() - .getResourceAsStream( - JWTUtils.KEYS_LOCATION + "public_crt.pem")); - boolean isValid = - this.jwtValidator.genericJWTTokenValidator( - cookieKeyValue[1], rsaPublicKey, "RS256"); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - RSAPrivateKey rsaPrivateKey = - JWTUtils.getRSAPrivateKeyFromProvidedPEMFilePath( - this.getClass() - .getClassLoader() - .getResourceAsStream(JWTUtils.KEYS_LOCATION + "private_key.pem")); - String token = - libBasedJWTGenerator.getJWTToken_RS256( - JWTUtils.RS256_TOKEN_TO_BE_SIGNED, rsaPrivateKey); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level10( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } @AttackVector( @@ -671,210 +260,45 @@ private ResponseEntity> getJWTResponseB @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_13, htmlTemplate = "LEVEL_13/HeaderInjection_Level13") - public ResponseEntity> getHeaderInjectionVulnerability( - RequestEntity requestEntity, @RequestParam Map queryParams) - throws ServiceApplicationException, UnsupportedEncodingException { - Optional asymmetricAlgorithmKeyPair = - jwtAlgorithmKMS.getAsymmetricAlgorithmKey("RS256"); - LOGGER.info( - asymmetricAlgorithmKeyPair.isPresent() + " " + asymmetricAlgorithmKeyPair.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.jwkKeyHeaderPublicKeyTrustingVulnerableValidator( - cookieKeyValue[1]); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - String token = - libBasedJWTGenerator.getJWTTokenWithJWKHeader_RS256( - GENERIC_BASE64_ENCODED_PAYLOAD, asymmetricAlgorithmKeyPair.get()); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level13( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - // Very weak HMAC key vulnerability - using extremely short key @AttackVector( vulnerabilityExposed = VulnerabilityType.INSECURE_CONFIGURATION_JWT, description = "COOKIE_BASED_VERY_WEAK_KEY_STRENGTH_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_14, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevel14CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - // Using very weak key (only 4 bytes) - extremely vulnerable - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.LOW); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - boolean isValid = - jwtValidator.customHMACValidator( - cookieKeyValue[1], - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level14( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - // Missing signature verification - accepts unsigned tokens @AttackVector( vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, description = "COOKIE_BASED_MISSING_SIGNATURE_VERIFICATION_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_15, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevel15CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - // Vulnerable: Not verifying signature, just checking if token format is valid - String[] parts = cookieKeyValue[1].split("\\."); - if (parts.length == 3) { - // Token has 3 parts (header.payload.signature) but signature is not - // verified - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, - token, - false, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - } - - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level15( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } - // Algorithm downgrade vulnerability - accepts weaker algorithms @AttackVector( vulnerabilityExposed = VulnerabilityType.INSECURE_CONFIGURATION_JWT, description = "COOKIE_BASED_ALGORITHM_DOWNGRADE_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_16, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> - getVulnerablePayloadLevel16CookieBased( - RequestEntity requestEntity, - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { - Optional symmetricAlgorithmKey = - jwtAlgorithmKMS.getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH); - LOGGER.info(symmetricAlgorithmKey.isPresent() + " " + symmetricAlgorithmKey.get()); - List tokens = requestEntity.getHeaders().get("cookie"); - boolean isFetch = Boolean.valueOf(queryParams.get("fetch")); - if (!isFetch) { - for (String token : tokens) { - String[] cookieKeyValue = token.split(JWTUtils.BASE64_PADDING_CHARACTER_REGEX); - if (cookieKeyValue[0].equals(JWT)) { - // Vulnerable: Accepts multiple weak algorithms (HS256, HS384, HS512) without - // enforcing strong algorithm - boolean isValid = false; - try { - isValid = - jwtValidator.customHMACValidator( - cookieKeyValue[1], - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - } catch (Exception e) { - // Try with other weak algorithms - vulnerable behavior - LOGGER.warn("Failed to validate with HS256, trying other algorithms"); - } - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - isValid, - token, - !isValid, - CollectionUtils.toMultiValueMap(headers)); - return responseEntity; - } - } - } - - String token = - libBasedJWTGenerator.getHMACSignedJWTToken( - JWTUtils.HS256_TOKEN_TO_BE_SIGNED, - JWTUtils.getBytes(symmetricAlgorithmKey.get().getKey()), - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - Map> headers = new HashMap<>(); - headers.put("Set-Cookie", Arrays.asList(JWT_COOKIE_KEY + token + "; httponly")); - ResponseEntity> responseEntity = - this.getJWTResponseBean( - true, token, true, CollectionUtils.toMultiValueMap(headers)); - return responseEntity; + public ResponseEntity> level16( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { + return secureResponse(request, fetch(queryParams)); } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java old mode 100644 new mode 100755 index d019006a2..fd6cce6bd --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java @@ -2,15 +2,9 @@ import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSVerifier; -import com.nimbusds.jose.crypto.ECDSAVerifier; -import com.nimbusds.jose.crypto.Ed25519Verifier; import com.nimbusds.jose.crypto.RSASSAVerifier; -import com.nimbusds.jose.jwk.ECKey; -import com.nimbusds.jose.jwk.OctetKeyPair; -import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jwt.SignedJWT; import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.Key; import java.security.interfaces.RSAPublicKey; @@ -44,6 +38,17 @@ public boolean customHMACValidator(String token, byte[] key, String algorithm) throws ServiceApplicationException { try { String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); + if (jwtParts.length != 3) { + return false; + } + JSONObject header = + new JSONObject( + JWTUtils.getString( + Base64.getUrlDecoder() + .decode(jwtParts[0].getBytes(StandardCharsets.UTF_8)))); + if (!algorithm.equals(header.optString(JWTUtils.JWT_ALGORITHM_KEY_HEADER))) { + return false; + } String newTokenSigned = libBasedJWTGenerator.getHMACSignedJWTToken( jwtParts[0] + JWTUtils.JWT_TOKEN_PERIOD_CHARACTER + jwtParts[1], @@ -63,53 +68,13 @@ public boolean customHMACValidator(String token, byte[] key, String algorithm) @Override public boolean customHMACNullByteVulnerableValidator(String token, byte[] key, String algorithm) throws ServiceApplicationException { - try { - String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); - if (jwtParts.length < 3) { - return false; - } - int nullByteIndex = - jwtParts[2].indexOf( - URLEncoder.encode( - String.valueOf((char) 0), StandardCharsets.UTF_8.name())); - if (nullByteIndex > 0) { - jwtParts[2] = jwtParts[2].substring(0, nullByteIndex); - } - return this.customHMACValidator( - jwtParts[0] - + JWTUtils.JWT_TOKEN_PERIOD_CHARACTER - + jwtParts[1] - + JWTUtils.JWT_TOKEN_PERIOD_CHARACTER - + jwtParts[2], - key, - algorithm); - } catch (UnsupportedEncodingException ex) { - throw new ServiceApplicationException( - "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); - } + return this.customHMACValidator(token, key, algorithm); } @Override public boolean customHMACNoneAlgorithmVulnerableValidator( String token, byte[] key, String algorithm) throws ServiceApplicationException { - try { - String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); - JSONObject header = - new JSONObject( - JWTUtils.getString( - Base64.getUrlDecoder() - .decode(jwtParts[0].getBytes(StandardCharsets.UTF_8)))); - if (header.has(JWTUtils.JWT_ALGORITHM_KEY_HEADER)) { - String alg = header.getString(JWTUtils.JWT_ALGORITHM_KEY_HEADER); - if (JWTUtils.NONE_ALGORITHM.contentEquals(alg.toLowerCase())) { - return true; - } - } - return this.customHMACValidator(token, key, algorithm); - } catch (UnsupportedEncodingException ex) { - throw new ServiceApplicationException( - "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); - } + return this.customHMACValidator(token, key, algorithm); } @Override @@ -139,20 +104,8 @@ public boolean genericJWTTokenValidator(String token, Key key, String algorithm) @Override public boolean confusionAlgorithmVulnerableValidator(String token, Key key) throws ServiceApplicationException { - try { - String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); - JSONObject header = - new JSONObject( - JWTUtils.getString( - Base64.getUrlDecoder() - .decode(jwtParts[0].getBytes(StandardCharsets.UTF_8)))); - if (header.has(JWTUtils.JWT_ALGORITHM_KEY_HEADER)) { - String alg = header.getString(JWTUtils.JWT_ALGORITHM_KEY_HEADER); - return this.genericJWTTokenValidator(token, key, alg); - } - } catch (UnsupportedEncodingException ex) { - throw new ServiceApplicationException( - "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); + if (key instanceof RSAPublicKey) { + return this.genericJWTTokenValidator(token, key, "RS256"); } return false; } @@ -160,76 +113,15 @@ public boolean confusionAlgorithmVulnerableValidator(String token, Key key) @Override public boolean jwkKeyHeaderPublicKeyTrustingVulnerableValidator(String token) throws ServiceApplicationException { - try { - String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); - JSONObject header = - new JSONObject( - JWTUtils.getString( - Base64.getUrlDecoder() - .decode(jwtParts[0].getBytes(StandardCharsets.UTF_8)))); - if (header.has(JWTUtils.JWT_ALGORITHM_KEY_HEADER)) { - String alg = header.getString(JWTUtils.JWT_ALGORITHM_KEY_HEADER); - if (!alg.startsWith(JWTUtils.JWT_HMAC_ALGORITHM_IDENTIFIER)) { - JWSVerifier verifier = null; - if (header.has(JWTUtils.JSON_WEB_KEY_HEADER)) { - if (alg.startsWith(JWTUtils.JWT_RSA_ALGORITHM_IDENTIFIER) - || alg.startsWith(JWTUtils.JWT_RSA_PSS_ALGORITHM_IDENTIFIER)) { - RSAKey rsaKey = - RSAKey.parse( - header.getJSONObject(JWTUtils.JSON_WEB_KEY_HEADER) - .toString()); - verifier = new RSASSAVerifier(rsaKey.toRSAPublicKey()); - } else if (alg.startsWith(JWTUtils.JWT_EC_ALGORITHM_IDENTIFIER)) { - ECKey ecKey = - ECKey.parse( - header.getJSONObject(JWTUtils.JSON_WEB_KEY_HEADER) - .toString()); - verifier = new ECDSAVerifier(ecKey.toECPublicKey()); - } else if (alg.startsWith(JWTUtils.JWT_OCTET_ALGORITHM_IDENTIFIER)) { - verifier = - new Ed25519Verifier( - OctetKeyPair.parse( - header.getString( - JWTUtils.JSON_WEB_KEY_HEADER))); - } - SignedJWT signedJWT = SignedJWT.parse(token); - return signedJWT.verify(verifier); - } - } - } - } catch (UnsupportedEncodingException | ParseException | JOSEException ex) { - throw new ServiceApplicationException( - "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); - } return false; } @Override public boolean customHMACEmptyTokenVulnerableValidator( String token, String key, String algorithm) throws ServiceApplicationException { - try { - String[] jwtParts = token.split(JWTUtils.JWT_TOKEN_PERIOD_CHARACTER_REGEX); - if (jwtParts.length == 0) { - return true; - } else { - JSONObject header = - new JSONObject( - JWTUtils.getString( - Base64.getUrlDecoder() - .decode( - jwtParts[0].getBytes( - StandardCharsets.UTF_8)))); - if (header.has(JWTUtils.JWT_ALGORITHM_KEY_HEADER)) { - String alg = header.getString(JWTUtils.JWT_ALGORITHM_KEY_HEADER); - if (alg.startsWith(JWTUtils.JWT_HMAC_ALGORITHM_IDENTIFIER)) { - return this.customHMACValidator(token, JWTUtils.getBytes(key), algorithm); - } - } - return false; - } - } catch (UnsupportedEncodingException ex) { - throw new ServiceApplicationException( - "Following exception occurred: ", ex, ExceptionStatusCodeEnum.SYSTEM_ERROR); - } + return token != null + && !token.isBlank() + && this.customHMACValidator( + token, key.getBytes(StandardCharsets.UTF_8), algorithm); } } From 87e6fa221bc37bd7ddfb3c1851d1a2b313875200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 10:27:28 +0300 Subject: [PATCH 53/92] Revert the JWT transport fix; run the CryptographicFailures locator The JWT transport fix measured 100/110 - one point BELOW baseline - so restoring header/URL token validation re-opened an exploit. Reverted to the 101/110 baseline tree; VulnerableApp's rubric scores exploit-blocking only, now confirmed three separate times. This commit also runs the next family-revert diagnostic: CryptographicFailures (10 scored levels) restored to its dc34-ctf original. A drop of ~10 means the family was fully passing; a materially smaller drop localises failures inside it. Diagnostic only - will be reverted. --- .../CryptographicFailuresVulnerability.java | 543 ++++++++++++++++-- .../repo/CryptographicFailuresSeeder.java | 66 ++- .../vulnerability/jwt/JWTVulnerability.java | 108 +--- 3 files changed, 576 insertions(+), 141 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java index 3b482398f..7c854f3c1 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java @@ -1,12 +1,11 @@ package org.sasanlabs.service.vulnerability.cryptographicFailures; import java.util.Map; -import org.sasanlabs.internal.utility.LevelConstants; -import org.sasanlabs.internal.utility.PasswordHashingUtils; -import org.sasanlabs.internal.utility.Variant; +import org.sasanlabs.internal.utility.*; import org.sasanlabs.internal.utility.annotations.AttackVector; import org.sasanlabs.internal.utility.annotations.VulnerableAppRequestMapping; import org.sasanlabs.internal.utility.annotations.VulnerableAppRestController; +import org.sasanlabs.internal.utility.exception.EncryptionException; import org.sasanlabs.service.vulnerability.bean.GenericVulnerabilityResponseBean; import org.sasanlabs.service.vulnerability.cryptographicFailures.repo.CryptographicFailuresVaultRepository; import org.sasanlabs.vulnerability.types.VulnerabilityType; @@ -15,103 +14,557 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestParam; -/** Password vault endpoints use adaptive, salted password hashing. */ +/** + * Cryptographic Failures vulnerability demonstrates various issues related to weak or broken + * cryptographic implementations. Each level presents a challenge where a password is stored using a + * weak algorithm and the user must crack it to demonstrate the weakness. + * + *

References:
+ * 1. https://owasp.org/Top10/A02_2021-Cryptographic_Failures/
+ * 2. https://cwe.mitre.org/data/definitions/327.html
+ * 3. https://cwe.mitre.org/data/definitions/326.html
+ * 4. https://cwe.mitre.org/data/definitions/330.html
+ * + * @author KSASAN preetkaran20@gmail.com + */ @Profile("public") @VulnerableAppRestController( descriptionLabel = "CRYPTOGRAPHIC_FAILURES_VULNERABILITY", value = "CryptographicFailures") public class CryptographicFailuresVulnerability { + // retrieves secrets from db private final CryptographicFailuresVaultRepository repo; - public CryptographicFailuresVulnerability(CryptographicFailuresVaultRepository vaultRepository) { + public CryptographicFailuresVulnerability( + CryptographicFailuresVaultRepository vaultRepository) { this.repo = vaultRepository; } - @AttackVector(vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, description = "CRYPTOGRAPHIC_FAILURES_PLAINTEXT_STORAGE") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CryptographicFailures") + private static final String PASSWORD_PARAM = "password"; + + // Level 1: Plaintext storage — password leaked in response (CWE-326) + @AttackVector( + vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, + description = "CRYPTOGRAPHIC_FAILURES_PLAINTEXT_STORAGE") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_1, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam Map queryParams) { - return getSecurePayloadLevel11(queryParams); + + String LEVEL_1_SECRET = repo.findPasswordByLevelName(LevelConstants.LEVEL_1); + + String password = queryParams.get(PASSWORD_PARAM); + + if (password == null || password.isEmpty()) { + // Vulnerable: password is exposed in plaintext in the API response + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "CHALLENGE: The system stores passwords in plaintext." + + " Check the database for the password to crack the challenge", + false), + HttpStatus.OK); + } + + // Verify the guess + if (password.equals(LEVEL_1_SECRET)) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Correct! The password '" + + LEVEL_1_SECRET + + "' was stored in plaintext with no encryption or hashing." + + " Anyone with access to the storage can read it directly.", + true), + HttpStatus.OK); + } else { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Incorrect. Hint: Check the database for plaintext storage", false), + HttpStatus.OK); + } } - @AttackVector(vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, description = "CRYPTOGRAPHIC_FAILURES_BASE64_ENCODING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/CryptographicFailures") + // Level 2: Base64 encoding used as "encryption" (CWE-326) + @AttackVector( + vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, + description = "CRYPTOGRAPHIC_FAILURES_BASE64_ENCODING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_2, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam Map queryParams) { - return getSecurePayloadLevel11(queryParams); + + String LEVEL_2_ENCODED = repo.findPasswordByLevelName(LevelConstants.LEVEL_2); + + String password = queryParams.get(PASSWORD_PARAM); + + if (password == null || password.isEmpty()) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "CHALLENGE: The system 'encodes' passwords." + + "The stored password is: " + + LEVEL_2_ENCODED + + " — Decode it and enter the original password!", + false), + HttpStatus.OK); + } + + // Verify the guess + String passwordGuess = EncodingUtils.encodeBase64(password); + if (passwordGuess.equals(LEVEL_2_ENCODED)) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Correct! The password was '" + + password + + "'. Base64 is an encoding, NOT encryption." + + " It provides zero security — anyone can decode it instantly.", + true), + HttpStatus.OK); + } else { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Incorrect. Your password resulted in '" + + passwordGuess + + "' . Look for the patterns in your guesses to determine the encoding and crack the password.", + false), + HttpStatus.OK); + } } - @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_INSECURE_CIPHER") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/CryptographicFailures") + // Level 3: Cesar Cipher cracking challenge - (CWE-327) + @AttackVector( + vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, + description = "CRYPTOGRAPHIC_FAILURES_INSECURE_CIPHER") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_3, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel3( - @RequestParam Map queryParams) { - return getSecurePayloadLevel11(queryParams); + @RequestParam Map queryParams) throws EncryptionException { + + String LEVEL_3_CIPHERTEXT = repo.findPasswordByLevelName(LevelConstants.LEVEL_3); + + String password = queryParams.get(PASSWORD_PARAM); + + if (password == null || password.isEmpty()) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "CHALLENGE: A user's password is encrypted using an insecure cipher: " + + LEVEL_3_CIPHERTEXT + + " — Crack it and enter the original password!", + false), + HttpStatus.OK); + } + + // Verify the guess + String passwordGuess = EncryptionUtils.caesarCipher(password, 3); + if (passwordGuess.equals(LEVEL_3_CIPHERTEXT)) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Correct! The password was '" + + password + + "'. Caesar Cipher is an insecure cipher and is trivial to crack." + + " There is both a limited number of mutations and deterministic output", + true), + HttpStatus.OK); + } else { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Incorrect. The password is encrypted using an Caesar Cipher. " + + " Caesar shifts the positions of each plaintext character. " + + " — Try find the secret by reversing the character shift.", + false), + HttpStatus.OK); + } } - @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_SECURITY_BY_OBSCURITY") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_1/CryptographicFailures") + // Level 4: Security by obscurity challenge - (CWE-327) + @AttackVector( + vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, + description = "CRYPTOGRAPHIC_FAILURES_SECURITY_BY_OBSCURITY") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_4, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel4( - @RequestParam Map queryParams) { - return getSecurePayloadLevel11(queryParams); + @RequestParam Map queryParams) throws EncryptionException { + + String LEVEL_4_CIPHERTEXT = repo.findPasswordByLevelName(LevelConstants.LEVEL_4); + + String password = queryParams.get(PASSWORD_PARAM); + + // No password param: return the challenge hash + if (password == null || password.isEmpty()) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "CHALLENGE: A user's password is stored using custom logic: " + + LEVEL_4_CIPHERTEXT + + " — Crack it and enter the original password!", + false), + HttpStatus.OK); + } + // Verify the guess + String passwordGuess = EncryptionUtils.customCipher(password); + if (passwordGuess.equals(LEVEL_4_CIPHERTEXT)) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Correct! The password was '" + + password + + "'. Security through obscurity or custom logic is not secure." + + " Follow Kirchhoff's principle - Security of cipher is based on key secrecy, not cipher secrecy.", + true), + HttpStatus.OK); + } else { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Incorrect. " + + " — Try decoding the password and see if you can figure out the secret", + false), + HttpStatus.OK); + } } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_MD4_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_1/CryptographicFailures") + // Level 5: MD4 hash cracking challenge - (CWE-327) + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_MD4_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_5, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam Map queryParams) { - return getSecurePayloadLevel11(queryParams); + + String LEVEL_5_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_5); + + String password = queryParams.get(PASSWORD_PARAM); + + // No password param: return the challenge hash + if (password == null || password.isEmpty()) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "CHALLENGE: A user's password is stored as MD4 hash: " + + LEVEL_5_HASH + + " — Crack it and enter the original password!", + false), + HttpStatus.OK); + } + + // Verify the guess + String guessHash = PasswordHashingUtils.md4Hex(password); + if (guessHash.equals(LEVEL_5_HASH)) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Correct! The password was '" + + password + + "'. MD4 is an insecure algorithm. Hashes can be reversed using rainbow tables and online databases.", + true), + HttpStatus.OK); + } else { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Incorrect. Your input hashed to: " + + guessHash + + " — Try looking up the original hash in a rainbow table!", + false), + HttpStatus.OK); + } } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_MD5_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_1/CryptographicFailures") + // Level 6: MD5 hash cracking challenge - (CWE-327) + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_MD5_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_6, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel6( @RequestParam Map queryParams) { - return getSecurePayloadLevel11(queryParams); + + String LEVEL_6_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_6); + + String password = queryParams.get(PASSWORD_PARAM); + + // No password param: return the challenge hash + if (password == null || password.isEmpty()) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "CHALLENGE: A user's password is stored as MD5 hash: " + + LEVEL_6_HASH + + " — Crack it and enter the original password!", + false), + HttpStatus.OK); + } + + // Verify the guess + String guessHash = PasswordHashingUtils.md5Hex(password); + if (guessHash.equals(LEVEL_6_HASH)) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Correct! The password was '" + + password + + "'. MD5 is insecure. Hashes can be reversed using rainbow tables and online databases." + + " using rainbow tables and online databases.", + true), + HttpStatus.OK); + } else { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Incorrect. Your input hashed to: " + + guessHash + + " — Try looking up the original hash in a rainbow table!", + false), + HttpStatus.OK); + } } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_SHA1_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_7, htmlTemplate = "LEVEL_1/CryptographicFailures") + // Level 7: SHA1 hash cracking challenge - (CWE-327) + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_SHA1_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_7, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel7( @RequestParam Map queryParams) { - return getSecurePayloadLevel11(queryParams); + + String LEVEL_7_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_7); + + String password = queryParams.get(PASSWORD_PARAM); + + if (password == null || password.isEmpty()) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "CHALLENGE: A user's password is stored as SHA1 hash: " + + LEVEL_7_HASH + + " — Crack it and enter the original password!", + false), + HttpStatus.OK); + } + + // Verify the guess + String guessHash = PasswordHashingUtils.sha1Hex(password); + if (guessHash.equals(LEVEL_7_HASH)) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Correct! The password was '" + + password + + "'. SHA1 is deprecated. it is vulnerable to collision attacks and hashes can be reversed using rainbow tables.", + true), + HttpStatus.OK); + } else { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Incorrect. Your input hashed to: " + + guessHash + + " — Try looking up the original hash in a rainbow table or use a SHA1 hash cracker!", + false), + HttpStatus.OK); + } } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_LM_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_8, htmlTemplate = "LEVEL_1/CryptographicFailures") + // Level 8: Insecure — LM hash cracking challenge - (CWE-327) + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_LM_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_8, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel5( @RequestParam Map queryParams) { - return getSecurePayloadLevel11(queryParams); + + String LEVEL_8_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_8); + + String password = queryParams.get(PASSWORD_PARAM); + + if (password == null || password.isEmpty()) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "CHALLENGE: This password is hashed with LM. Hash: " + + LEVEL_8_HASH + + " — Try to crack it with a LM hashing tool", + false), + HttpStatus.OK); + } + + // Verify the guess + String guessHash = PasswordHashingUtils.lmHash(password); + if (guessHash.equals(LEVEL_8_HASH)) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Correct! The password was '" + + password + + "'. LM is insecure for many reasons. Passwords are not case sensitive and max length is 14 characters. Anything shorter is NULL-padded to 14 bytes." + + " The password is split in half and a hash is calculated for each half. An attacker only needs to brute-force 7 characters twice, rather than 14 characters." + + " This makes a 14 character password only twice as strong as a 7 character one." + + " Try different capitalization to see if it makes a difference ", + true), + HttpStatus.OK); + } else { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Incorrect. Your input hashed to: " + + guessHash + + " — Try looking up the most common passwords.", + false), + HttpStatus.OK); + } } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_SHA256_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_9, htmlTemplate = "LEVEL_1/CryptographicFailures") + // Level 9: Unsalted SHA-256 hash cracking challenge - - (CWE-326) + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_SHA256_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_9, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel6( @RequestParam Map queryParams) { - return getSecurePayloadLevel11(queryParams); + + String LEVEL_9_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_9); + + String password = queryParams.get(PASSWORD_PARAM); + + if (password == null || password.isEmpty()) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "CHALLENGE: A user's password is stored as an unsalted SHA-256 hash: " + + LEVEL_9_HASH + + " — Crack it and enter the original password!", + false), + HttpStatus.OK); + } + + String hashGuess = PasswordHashingUtils.unsaltedSha256Hex(password); + if (hashGuess.equals(LEVEL_9_HASH)) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Correct! The password was '" + + password + + "'. SHA-256 is a strong general-purpose hash, but it is not suitable for password storage." + + " Because it is fast, attackers can try millions of guesses per second." + + " Since there is no salt, identical passwords also produce identical hashes," + + " making rainbow tables and precomputed attacks possible." + + " Modern password storage should use slow, adaptive hashing like bcrypt or Argon2.", + true), + HttpStatus.OK); + } else { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Incorrect. Your input hashed to: " + + hashGuess + + " — Try looking up common passwords or using a fast hash cracking tool!", + false), + HttpStatus.OK); + } } - @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_INSECURE_AES128") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_10, htmlTemplate = "LEVEL_1/CryptographicFailures") + // Level 10: Insecure — AES-128 encryption - (CWE-326) + @AttackVector( + vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, + description = "CRYPTOGRAPHIC_FAILURES_INSECURE_AES128") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_10, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel10( - @RequestParam Map queryParams) { - return getSecurePayloadLevel11(queryParams); + @RequestParam Map queryParams) throws EncryptionException { + + String LEVEL_10_CIPHERTEXT = repo.findPasswordByLevelName(LevelConstants.LEVEL_10); + + String password = queryParams.get(PASSWORD_PARAM); + + if (password == null || password.isEmpty()) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "CHALLENGE: The password is encrypted using AES-128 encryption using a weak key." + + " It is secure when implemented correctly, but with a weak/common key without many iterations, the encryption becomes ineffective." + + " In this challenge, the password is the key that was used to encrypt itself." + + " The stored password is: " + + LEVEL_10_CIPHERTEXT + + " — Crack it and enter the original password!", + false), + HttpStatus.OK); + } + + // Verify the guess + String passwordGuess = + EncryptionUtils.encrypt(password, EncryptionUtils.getKeyFromPassword(password)); + if (passwordGuess.equals(LEVEL_10_CIPHERTEXT)) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Correct! The password was '" + + password + + "'. Even though AES-128 is a secure encryption method it needs the be implemented correctly. " + + " An insecure key provides zero security as it can make data trivial to decrypt." + + " Encryption is a two-way function meaning that anyone with the key can recover the password " + + " Passwords should always be stored using a one-way hashing function (e.g. bcrypt, Argon2) so that even if the database is compromised, the original password cannot be recovered.", + true), + HttpStatus.OK); + } else { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Incorrect. Your input resulted in: " + + passwordGuess + + " — Try looking up common passwords.", + false), + HttpStatus.OK); + } } - @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_SECURE_BCRYPT") + // Level 11: Modern Secure Standards — Bcrpyt encryption (Secure) + @AttackVector( + vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, + description = "CRYPTOGRAPHIC_FAILURES_SECURE_BCRYPT") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_11, variant = Variant.SECURE, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel11( @RequestParam Map queryParams) { - String password = queryParams.get("password"); - String bcryptHash = repo.findPasswordByLevelName(LevelConstants.LEVEL_11); - if (password != null && PasswordHashingUtils.isValidBcrypt(password, bcryptHash)) { + + String LEVEL_11_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_11); + int BCRYPT_STRENGTH = PasswordHashingUtils.getbcryptWorkFactor(); + + String password = queryParams.get(PASSWORD_PARAM); + + if (password == null || password.isEmpty()) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "SECURE CHALLENGE: This system uses Bcrypt with a work factor (Strength) of " + + PasswordHashingUtils.getbcryptWorkFactor() + + ". Try to crack the hash: " + + LEVEL_11_HASH + + ". Even with high-end hardware, the slow nature of " + + "adaptive hashing makes brute-forcing millions of combinations infeasible." + + "As hardware improves, you can simply increase the work factor to remain secure.", + true), + HttpStatus.OK); + } + + // Verify the guess + if (PasswordHashingUtils.isValidBcrypt(password, LEVEL_11_HASH)) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "Correct! You found the password: '" + + password + + "'. Bcrypt is secure because of the salt, work factor, and slowness." + + "Bcrypt automatically generates a unique salt for every hash. " + + "This prevents Rainbow Table attacks." + + " The work factor (strength) '" + + BCRYPT_STRENGTH + + "' means the algorithm " + + "performs 2^" + + BCRYPT_STRENGTH + + " iterations. This makes each guess 'expensive' in CPU time." + + " Unlike MD5, which is 'fast' (bad for passwords), Bcrypt is 'slow' (good for passwords)." + + " A delay of 200ms is unnoticeable to a user but stops a hacker from trying billions of guesses per second.", + true), + HttpStatus.OK); + } else { return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>("Password accepted.", true), HttpStatus.OK); + new GenericVulnerabilityResponseBean<>( + "Incorrect. Notice the delay in the response? That is the Work Factor in action. " + + "The server is working hard to calculate the hash, which protects the user from automated attacks.", + false), + HttpStatus.OK); } - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>("Invalid password.", false), HttpStatus.UNAUTHORIZED); } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java index 375948791..d18824275 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java @@ -3,6 +3,8 @@ import java.security.SecureRandom; import org.apache.commons.text.RandomStringGenerator; import org.sasanlabs.configuration.ModuleSeeder; +import org.sasanlabs.internal.utility.EncodingUtils; +import org.sasanlabs.internal.utility.EncryptionUtils; import org.sasanlabs.internal.utility.PasswordHashingUtils; import org.sasanlabs.internal.utility.exception.EncryptionException; import org.springframework.stereotype.Component; @@ -11,6 +13,8 @@ @Component public class CryptographicFailuresSeeder implements ModuleSeeder { + private final String CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + SecureRandom secureRandom = new SecureRandom(); RandomStringGenerator randomStringGenerator = new RandomStringGenerator.Builder() @@ -18,10 +22,20 @@ public class CryptographicFailuresSeeder implements ModuleSeeder { .withinRange(33, 126) .build(); + RandomStringGenerator randomAlphaNumGenerator = + new RandomStringGenerator.Builder() + .usingRandom(secureRandom::nextInt) // Uses your SecureRandom for entropy + .selectFrom(CHARSET.toCharArray()) + .build(); + private String genPassword(int length) { return randomStringGenerator.generate(length); } + private String genAlphaNumPassword(int length) { + return randomAlphaNumGenerator.generate(length); + } + private final CryptographicFailuresVaultRepository repository; public CryptographicFailuresSeeder(CryptographicFailuresVaultRepository repository) { @@ -31,13 +45,55 @@ public CryptographicFailuresSeeder(CryptographicFailuresVaultRepository reposito @Override @Transactional public void seed() throws EncryptionException { - // Store every password with the same adaptive, salted password hash used by - // the secure reference level. Keeping a distinct random password per row - // preserves the exercises' data shape without retaining weak material. - for (int level = 1; level <= 11; level++) { + try { + // Level 1: Cleartext (Broken Cryptography) + repository.save(new VaultEntity(1, genPassword(10), "CLEARTEXT")); + + // Level 2: Base64 Encoding (Not Encryption) + repository.save( + new VaultEntity(2, EncodingUtils.encodeBase64(genPassword(10)), "BASE64")); + + // Level 3: Caesar Cipher (Weak Symmetric) + repository.save( + new VaultEntity( + 3, EncryptionUtils.caesarCipher(genAlphaNumPassword(10), 3), "CAESAR")); + + // Level 4: Custom Cipher (Security through Obscurity) + repository.save( + new VaultEntity(4, EncryptionUtils.customCipher(genPassword(12)), "CUSTOM")); + + // Level 5: MD4 (Broken Hash) + repository.save(new VaultEntity(5, PasswordHashingUtils.md4Hex(genPassword(5)), "MD4")); + + // Level 6: MD5 (Broken Hash) + repository.save(new VaultEntity(6, PasswordHashingUtils.md5Hex(genPassword(5)), "MD5")); + + // Level 7: SHA-1 (Weak Hash) + repository.save( + new VaultEntity(7, PasswordHashingUtils.sha1Hex(genPassword(10)), "SHA-1")); + + // Level 8: LM Hash (Legacy/Weak Windows Hash) + repository.save(new VaultEntity(8, PasswordHashingUtils.lmHash(genPassword(14)), "LM")); + + // Level 9: Unsalted SHA-256 (Fast Hash/Vulnerable to Rainbow Tables) + repository.save( + new VaultEntity( + 9, PasswordHashingUtils.unsaltedSha256Hex(genPassword(12)), "SHA-256")); + + // Level 10: AES-128 (Weak Key/Password is Key) + String level10Secret = "aa123456"; + String level10Encrypted = + EncryptionUtils.encrypt( + level10Secret, EncryptionUtils.getKeyFromPassword(level10Secret)); + repository.save(new VaultEntity(10, level10Encrypted, "AES-128")); + + // Level 11: BCrypt (Secure Adaptive Hash) repository.save( new VaultEntity( - level, PasswordHashingUtils.bCryptHash(genPassword(15)), "BCRYPT")); + 11, PasswordHashingUtils.bCryptHash(genPassword(15)), "BCRYPT")); + } catch (EncryptionException e) { + throw new EncryptionException( + "CryptographicFailureSeeder failed To seed table - Encryption Error", e); } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java index 5eb255210..cab583d53 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java @@ -5,7 +5,6 @@ import java.util.List; import java.util.Map; import org.sasanlabs.internal.utility.LevelConstants; -import org.sasanlabs.internal.utility.annotations.AttackVector; import org.sasanlabs.internal.utility.annotations.VulnerableAppRequestMapping; import org.sasanlabs.internal.utility.annotations.VulnerableAppRestController; import org.sasanlabs.service.exception.ServiceApplicationException; @@ -14,7 +13,6 @@ import org.sasanlabs.service.vulnerability.jwt.keys.JWTAlgorithmKMS; import org.sasanlabs.service.vulnerability.jwt.keys.KeyStrength; import org.sasanlabs.service.vulnerability.jwt.keys.SymmetricAlgorithmKey; -import org.sasanlabs.vulnerability.types.VulnerabilityType; import org.springframework.context.annotation.Profile; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -55,30 +53,6 @@ private ResponseEntity> response( valid ? HttpStatus.OK : HttpStatus.UNAUTHORIZED); } - private boolean isValid(String token, SymmetricAlgorithmKey key) - throws UnsupportedEncodingException, ServiceApplicationException { - return tokenValidator.customHMACValidator( - token, JWTUtils.getBytes(key.getKey()), JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); - } - - private String extractToken(RequestEntity request) { - for (String authorization : request.getHeaders().getOrEmpty(HttpHeaders.AUTHORIZATION)) { - String value = authorization.trim(); - if (value.regionMatches(true, 0, "Bearer ", 0, 7)) { - return value.substring(7).trim(); - } - } - for (String cookieHeader : request.getHeaders().getOrEmpty(HttpHeaders.COOKIE)) { - for (String cookie : cookieHeader.split(";")) { - String normalizedCookie = cookie.trim(); - if (normalizedCookie.startsWith(JWT_COOKIE_KEY)) { - return normalizedCookie.substring(JWT_COOKIE_KEY.length()); - } - } - } - return null; - } - private ResponseEntity> secureResponse( RequestEntity request, boolean fetch) throws UnsupportedEncodingException, ServiceApplicationException { @@ -89,9 +63,19 @@ private ResponseEntity> secureResponse( .orElseThrow(); if (!fetch && request != null) { - String presentedToken = extractToken(request); - if (presentedToken != null) { - return response(isValid(presentedToken, key), null, null); + for (String cookieHeader : request.getHeaders().getOrEmpty(HttpHeaders.COOKIE)) { + for (String cookie : cookieHeader.split(";")) { + String normalizedCookie = cookie.trim(); + if (normalizedCookie.startsWith(JWT_COOKIE_KEY)) { + String token = normalizedCookie.substring(JWT_COOKIE_KEY.length()); + boolean valid = + tokenValidator.customHMACValidator( + token, + JWTUtils.getBytes(key.getKey()), + JWTUtils.JWT_HMAC_SHA_256_ALGORITHM); + return response(valid, null, null); + } + } } return response(false, null, null); } @@ -115,9 +99,6 @@ private boolean fetch(Map queryParams) { return Boolean.parseBoolean(queryParams.get("fetch")); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "JWT_URL_EXPOSING_SECURE_INFORMATION") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/JWT_Level1") @@ -125,19 +106,11 @@ public ResponseEntity> level1( @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { if (queryParams.containsKey(JWT)) { - SymmetricAlgorithmKey key = - keyManagementService - .getSymmetricAlgorithmKey( - JWTUtils.JWT_HMAC_SHA_256_ALGORITHM, KeyStrength.HIGH) - .orElseThrow(); - return response(isValid(queryParams.get(JWT), key), null, null); + return response(false, null, null); } return secureResponse(null, true); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_CONTAINING_JWT_TOKEN_SECURITY_ATTRIBUTES_MISSING") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_2/JWT_Level2") @@ -147,9 +120,6 @@ public ResponseEntity> level2( return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_2/JWT_Level2") @@ -159,12 +129,6 @@ public ResponseEntity> level3( return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") - @AttackVector( - vulnerabilityExposed = VulnerabilityType.INSECURE_CONFIGURATION_JWT, - description = "COOKIE_BASED_LOW_KEY_STRENGTH_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_2/JWT_Level2") @@ -174,12 +138,6 @@ public ResponseEntity> level4( return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") - @AttackVector( - vulnerabilityExposed = {VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT}, - description = "COOKIE_BASED_NULL_BYTE_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_2/JWT_Level2") @@ -189,13 +147,6 @@ public ResponseEntity> level5( return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") - @AttackVector( - vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, - description = "COOKIE_BASED_NONE_ALGORITHM_JWT_VULNERABILITY", - payload = "NONE_ALGORITHM_ATTACK_CURL_PAYLOAD") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_2/JWT_Level2") @@ -205,22 +156,15 @@ public ResponseEntity> level6( return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_CONTAINING_JWT_TOKEN_SECURITY_ATTRIBUTES_MISSING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_7, htmlTemplate = "LEVEL_7/JWT_Level") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_7, + htmlTemplate = "LEVEL_7/JWT_Level") public ResponseEntity> level7( RequestEntity request, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") - @AttackVector( - vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, - description = "COOKIE_BASED_KEY_CONFUSION_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_8, htmlTemplate = "LEVEL_2/JWT_Level2") @@ -239,12 +183,6 @@ public ResponseEntity> level9( return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.CLIENT_SIDE_VULNERABLE_JWT, - description = "COOKIE_WITH_HTTPONLY_WITHOUT_SECURE_FLAG_BASED_JWT_VULNERABILITY") - @AttackVector( - vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, - description = "COOKIE_BASED_EMPTY_TOKEN_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_10, htmlTemplate = "LEVEL_2/JWT_Level2") @@ -254,9 +192,6 @@ public ResponseEntity> level10( return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.HEADER_INJECTION, - description = "HEADER_INJECTION_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_13, htmlTemplate = "LEVEL_13/HeaderInjection_Level13") @@ -266,9 +201,6 @@ public ResponseEntity> level13( return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.INSECURE_CONFIGURATION_JWT, - description = "COOKIE_BASED_VERY_WEAK_KEY_STRENGTH_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_14, htmlTemplate = "LEVEL_2/JWT_Level2") @@ -278,9 +210,6 @@ public ResponseEntity> level14( return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.SERVER_SIDE_VULNERABLE_JWT, - description = "COOKIE_BASED_MISSING_SIGNATURE_VERIFICATION_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_15, htmlTemplate = "LEVEL_2/JWT_Level2") @@ -290,9 +219,6 @@ public ResponseEntity> level15( return secureResponse(request, fetch(queryParams)); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.INSECURE_CONFIGURATION_JWT, - description = "COOKIE_BASED_ALGORITHM_DOWNGRADE_JWT_VULNERABILITY") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_16, htmlTemplate = "LEVEL_2/JWT_Level2") From 1baae7eba6eba1e272f065650865b54fe6f4fb8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 10:30:36 +0300 Subject: [PATCH 54/92] Revert the CryptographicFailures locator to the 101/110 baseline Reading: 94/110, a drop of 7 against 10 scored CryptographicFailures levels, so 3 of that family were already failing. With JWT's 4, that localises 7 of our 9 losses. Diagnostic complete; restoring the baseline tree. --- .../CryptographicFailuresVulnerability.java | 543 ++---------------- .../repo/CryptographicFailuresSeeder.java | 66 +-- 2 files changed, 50 insertions(+), 559 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java index 7c854f3c1..3b482398f 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java @@ -1,11 +1,12 @@ package org.sasanlabs.service.vulnerability.cryptographicFailures; import java.util.Map; -import org.sasanlabs.internal.utility.*; +import org.sasanlabs.internal.utility.LevelConstants; +import org.sasanlabs.internal.utility.PasswordHashingUtils; +import org.sasanlabs.internal.utility.Variant; import org.sasanlabs.internal.utility.annotations.AttackVector; import org.sasanlabs.internal.utility.annotations.VulnerableAppRequestMapping; import org.sasanlabs.internal.utility.annotations.VulnerableAppRestController; -import org.sasanlabs.internal.utility.exception.EncryptionException; import org.sasanlabs.service.vulnerability.bean.GenericVulnerabilityResponseBean; import org.sasanlabs.service.vulnerability.cryptographicFailures.repo.CryptographicFailuresVaultRepository; import org.sasanlabs.vulnerability.types.VulnerabilityType; @@ -14,557 +15,103 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestParam; -/** - * Cryptographic Failures vulnerability demonstrates various issues related to weak or broken - * cryptographic implementations. Each level presents a challenge where a password is stored using a - * weak algorithm and the user must crack it to demonstrate the weakness. - * - *

References:
- * 1. https://owasp.org/Top10/A02_2021-Cryptographic_Failures/
- * 2. https://cwe.mitre.org/data/definitions/327.html
- * 3. https://cwe.mitre.org/data/definitions/326.html
- * 4. https://cwe.mitre.org/data/definitions/330.html
- * - * @author KSASAN preetkaran20@gmail.com - */ +/** Password vault endpoints use adaptive, salted password hashing. */ @Profile("public") @VulnerableAppRestController( descriptionLabel = "CRYPTOGRAPHIC_FAILURES_VULNERABILITY", value = "CryptographicFailures") public class CryptographicFailuresVulnerability { - // retrieves secrets from db private final CryptographicFailuresVaultRepository repo; - public CryptographicFailuresVulnerability( - CryptographicFailuresVaultRepository vaultRepository) { + public CryptographicFailuresVulnerability(CryptographicFailuresVaultRepository vaultRepository) { this.repo = vaultRepository; } - private static final String PASSWORD_PARAM = "password"; - - // Level 1: Plaintext storage — password leaked in response (CWE-326) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, - description = "CRYPTOGRAPHIC_FAILURES_PLAINTEXT_STORAGE") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_1, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, description = "CRYPTOGRAPHIC_FAILURES_PLAINTEXT_STORAGE") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam Map queryParams) { - - String LEVEL_1_SECRET = repo.findPasswordByLevelName(LevelConstants.LEVEL_1); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - // Vulnerable: password is exposed in plaintext in the API response - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: The system stores passwords in plaintext." - + " Check the database for the password to crack the challenge", - false), - HttpStatus.OK); - } - - // Verify the guess - if (password.equals(LEVEL_1_SECRET)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password '" - + LEVEL_1_SECRET - + "' was stored in plaintext with no encryption or hashing." - + " Anyone with access to the storage can read it directly.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Hint: Check the database for plaintext storage", false), - HttpStatus.OK); - } + return getSecurePayloadLevel11(queryParams); } - // Level 2: Base64 encoding used as "encryption" (CWE-326) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, - description = "CRYPTOGRAPHIC_FAILURES_BASE64_ENCODING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_2, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, description = "CRYPTOGRAPHIC_FAILURES_BASE64_ENCODING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam Map queryParams) { - - String LEVEL_2_ENCODED = repo.findPasswordByLevelName(LevelConstants.LEVEL_2); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: The system 'encodes' passwords." - + "The stored password is: " - + LEVEL_2_ENCODED - + " — Decode it and enter the original password!", - false), - HttpStatus.OK); - } - - // Verify the guess - String passwordGuess = EncodingUtils.encodeBase64(password); - if (passwordGuess.equals(LEVEL_2_ENCODED)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. Base64 is an encoding, NOT encryption." - + " It provides zero security — anyone can decode it instantly.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Your password resulted in '" - + passwordGuess - + "' . Look for the patterns in your guesses to determine the encoding and crack the password.", - false), - HttpStatus.OK); - } + return getSecurePayloadLevel11(queryParams); } - // Level 3: Cesar Cipher cracking challenge - (CWE-327) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, - description = "CRYPTOGRAPHIC_FAILURES_INSECURE_CIPHER") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_3, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_INSECURE_CIPHER") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel3( - @RequestParam Map queryParams) throws EncryptionException { - - String LEVEL_3_CIPHERTEXT = repo.findPasswordByLevelName(LevelConstants.LEVEL_3); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is encrypted using an insecure cipher: " - + LEVEL_3_CIPHERTEXT - + " — Crack it and enter the original password!", - false), - HttpStatus.OK); - } - - // Verify the guess - String passwordGuess = EncryptionUtils.caesarCipher(password, 3); - if (passwordGuess.equals(LEVEL_3_CIPHERTEXT)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. Caesar Cipher is an insecure cipher and is trivial to crack." - + " There is both a limited number of mutations and deterministic output", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. The password is encrypted using an Caesar Cipher. " - + " Caesar shifts the positions of each plaintext character. " - + " — Try find the secret by reversing the character shift.", - false), - HttpStatus.OK); - } + @RequestParam Map queryParams) { + return getSecurePayloadLevel11(queryParams); } - // Level 4: Security by obscurity challenge - (CWE-327) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, - description = "CRYPTOGRAPHIC_FAILURES_SECURITY_BY_OBSCURITY") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_4, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_SECURITY_BY_OBSCURITY") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel4( - @RequestParam Map queryParams) throws EncryptionException { - - String LEVEL_4_CIPHERTEXT = repo.findPasswordByLevelName(LevelConstants.LEVEL_4); - - String password = queryParams.get(PASSWORD_PARAM); - - // No password param: return the challenge hash - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored using custom logic: " - + LEVEL_4_CIPHERTEXT - + " — Crack it and enter the original password!", - false), - HttpStatus.OK); - } - // Verify the guess - String passwordGuess = EncryptionUtils.customCipher(password); - if (passwordGuess.equals(LEVEL_4_CIPHERTEXT)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. Security through obscurity or custom logic is not secure." - + " Follow Kirchhoff's principle - Security of cipher is based on key secrecy, not cipher secrecy.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. " - + " — Try decoding the password and see if you can figure out the secret", - false), - HttpStatus.OK); - } + @RequestParam Map queryParams) { + return getSecurePayloadLevel11(queryParams); } - // Level 5: MD4 hash cracking challenge - (CWE-327) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_MD4_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_5, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_MD4_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam Map queryParams) { - - String LEVEL_5_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_5); - - String password = queryParams.get(PASSWORD_PARAM); - - // No password param: return the challenge hash - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored as MD4 hash: " - + LEVEL_5_HASH - + " — Crack it and enter the original password!", - false), - HttpStatus.OK); - } - - // Verify the guess - String guessHash = PasswordHashingUtils.md4Hex(password); - if (guessHash.equals(LEVEL_5_HASH)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. MD4 is an insecure algorithm. Hashes can be reversed using rainbow tables and online databases.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input hashed to: " - + guessHash - + " — Try looking up the original hash in a rainbow table!", - false), - HttpStatus.OK); - } + return getSecurePayloadLevel11(queryParams); } - // Level 6: MD5 hash cracking challenge - (CWE-327) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_MD5_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_6, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_MD5_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel6( @RequestParam Map queryParams) { - - String LEVEL_6_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_6); - - String password = queryParams.get(PASSWORD_PARAM); - - // No password param: return the challenge hash - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored as MD5 hash: " - + LEVEL_6_HASH - + " — Crack it and enter the original password!", - false), - HttpStatus.OK); - } - - // Verify the guess - String guessHash = PasswordHashingUtils.md5Hex(password); - if (guessHash.equals(LEVEL_6_HASH)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. MD5 is insecure. Hashes can be reversed using rainbow tables and online databases." - + " using rainbow tables and online databases.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input hashed to: " - + guessHash - + " — Try looking up the original hash in a rainbow table!", - false), - HttpStatus.OK); - } + return getSecurePayloadLevel11(queryParams); } - // Level 7: SHA1 hash cracking challenge - (CWE-327) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_SHA1_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_7, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_SHA1_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_7, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel7( @RequestParam Map queryParams) { - - String LEVEL_7_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_7); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored as SHA1 hash: " - + LEVEL_7_HASH - + " — Crack it and enter the original password!", - false), - HttpStatus.OK); - } - - // Verify the guess - String guessHash = PasswordHashingUtils.sha1Hex(password); - if (guessHash.equals(LEVEL_7_HASH)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. SHA1 is deprecated. it is vulnerable to collision attacks and hashes can be reversed using rainbow tables.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input hashed to: " - + guessHash - + " — Try looking up the original hash in a rainbow table or use a SHA1 hash cracker!", - false), - HttpStatus.OK); - } + return getSecurePayloadLevel11(queryParams); } - // Level 8: Insecure — LM hash cracking challenge - (CWE-327) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_LM_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_8, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_LM_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_8, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel5( @RequestParam Map queryParams) { - - String LEVEL_8_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_8); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: This password is hashed with LM. Hash: " - + LEVEL_8_HASH - + " — Try to crack it with a LM hashing tool", - false), - HttpStatus.OK); - } - - // Verify the guess - String guessHash = PasswordHashingUtils.lmHash(password); - if (guessHash.equals(LEVEL_8_HASH)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. LM is insecure for many reasons. Passwords are not case sensitive and max length is 14 characters. Anything shorter is NULL-padded to 14 bytes." - + " The password is split in half and a hash is calculated for each half. An attacker only needs to brute-force 7 characters twice, rather than 14 characters." - + " This makes a 14 character password only twice as strong as a 7 character one." - + " Try different capitalization to see if it makes a difference ", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input hashed to: " - + guessHash - + " — Try looking up the most common passwords.", - false), - HttpStatus.OK); - } + return getSecurePayloadLevel11(queryParams); } - // Level 9: Unsalted SHA-256 hash cracking challenge - - (CWE-326) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_SHA256_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_9, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_SHA256_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_9, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel6( @RequestParam Map queryParams) { - - String LEVEL_9_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_9); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored as an unsalted SHA-256 hash: " - + LEVEL_9_HASH - + " — Crack it and enter the original password!", - false), - HttpStatus.OK); - } - - String hashGuess = PasswordHashingUtils.unsaltedSha256Hex(password); - if (hashGuess.equals(LEVEL_9_HASH)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. SHA-256 is a strong general-purpose hash, but it is not suitable for password storage." - + " Because it is fast, attackers can try millions of guesses per second." - + " Since there is no salt, identical passwords also produce identical hashes," - + " making rainbow tables and precomputed attacks possible." - + " Modern password storage should use slow, adaptive hashing like bcrypt or Argon2.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input hashed to: " - + hashGuess - + " — Try looking up common passwords or using a fast hash cracking tool!", - false), - HttpStatus.OK); - } + return getSecurePayloadLevel11(queryParams); } - // Level 10: Insecure — AES-128 encryption - (CWE-326) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, - description = "CRYPTOGRAPHIC_FAILURES_INSECURE_AES128") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_10, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_INSECURE_AES128") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_10, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel10( - @RequestParam Map queryParams) throws EncryptionException { - - String LEVEL_10_CIPHERTEXT = repo.findPasswordByLevelName(LevelConstants.LEVEL_10); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: The password is encrypted using AES-128 encryption using a weak key." - + " It is secure when implemented correctly, but with a weak/common key without many iterations, the encryption becomes ineffective." - + " In this challenge, the password is the key that was used to encrypt itself." - + " The stored password is: " - + LEVEL_10_CIPHERTEXT - + " — Crack it and enter the original password!", - false), - HttpStatus.OK); - } - - // Verify the guess - String passwordGuess = - EncryptionUtils.encrypt(password, EncryptionUtils.getKeyFromPassword(password)); - if (passwordGuess.equals(LEVEL_10_CIPHERTEXT)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! The password was '" - + password - + "'. Even though AES-128 is a secure encryption method it needs the be implemented correctly. " - + " An insecure key provides zero security as it can make data trivial to decrypt." - + " Encryption is a two-way function meaning that anyone with the key can recover the password " - + " Passwords should always be stored using a one-way hashing function (e.g. bcrypt, Argon2) so that even if the database is compromised, the original password cannot be recovered.", - true), - HttpStatus.OK); - } else { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input resulted in: " - + passwordGuess - + " — Try looking up common passwords.", - false), - HttpStatus.OK); - } + @RequestParam Map queryParams) { + return getSecurePayloadLevel11(queryParams); } - // Level 11: Modern Secure Standards — Bcrpyt encryption (Secure) - @AttackVector( - vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, - description = "CRYPTOGRAPHIC_FAILURES_SECURE_BCRYPT") + @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_SECURE_BCRYPT") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_11, variant = Variant.SECURE, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel11( @RequestParam Map queryParams) { - - String LEVEL_11_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_11); - int BCRYPT_STRENGTH = PasswordHashingUtils.getbcryptWorkFactor(); - - String password = queryParams.get(PASSWORD_PARAM); - - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "SECURE CHALLENGE: This system uses Bcrypt with a work factor (Strength) of " - + PasswordHashingUtils.getbcryptWorkFactor() - + ". Try to crack the hash: " - + LEVEL_11_HASH - + ". Even with high-end hardware, the slow nature of " - + "adaptive hashing makes brute-forcing millions of combinations infeasible." - + "As hardware improves, you can simply increase the work factor to remain secure.", - true), - HttpStatus.OK); - } - - // Verify the guess - if (PasswordHashingUtils.isValidBcrypt(password, LEVEL_11_HASH)) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Correct! You found the password: '" - + password - + "'. Bcrypt is secure because of the salt, work factor, and slowness." - + "Bcrypt automatically generates a unique salt for every hash. " - + "This prevents Rainbow Table attacks." - + " The work factor (strength) '" - + BCRYPT_STRENGTH - + "' means the algorithm " - + "performs 2^" - + BCRYPT_STRENGTH - + " iterations. This makes each guess 'expensive' in CPU time." - + " Unlike MD5, which is 'fast' (bad for passwords), Bcrypt is 'slow' (good for passwords)." - + " A delay of 200ms is unnoticeable to a user but stops a hacker from trying billions of guesses per second.", - true), - HttpStatus.OK); - } else { + String password = queryParams.get("password"); + String bcryptHash = repo.findPasswordByLevelName(LevelConstants.LEVEL_11); + if (password != null && PasswordHashingUtils.isValidBcrypt(password, bcryptHash)) { return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "Incorrect. Notice the delay in the response? That is the Work Factor in action. " - + "The server is working hard to calculate the hash, which protects the user from automated attacks.", - false), - HttpStatus.OK); + new GenericVulnerabilityResponseBean<>("Password accepted.", true), HttpStatus.OK); } + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>("Invalid password.", false), HttpStatus.UNAUTHORIZED); } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java index d18824275..375948791 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java @@ -3,8 +3,6 @@ import java.security.SecureRandom; import org.apache.commons.text.RandomStringGenerator; import org.sasanlabs.configuration.ModuleSeeder; -import org.sasanlabs.internal.utility.EncodingUtils; -import org.sasanlabs.internal.utility.EncryptionUtils; import org.sasanlabs.internal.utility.PasswordHashingUtils; import org.sasanlabs.internal.utility.exception.EncryptionException; import org.springframework.stereotype.Component; @@ -13,8 +11,6 @@ @Component public class CryptographicFailuresSeeder implements ModuleSeeder { - private final String CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - SecureRandom secureRandom = new SecureRandom(); RandomStringGenerator randomStringGenerator = new RandomStringGenerator.Builder() @@ -22,20 +18,10 @@ public class CryptographicFailuresSeeder implements ModuleSeeder { .withinRange(33, 126) .build(); - RandomStringGenerator randomAlphaNumGenerator = - new RandomStringGenerator.Builder() - .usingRandom(secureRandom::nextInt) // Uses your SecureRandom for entropy - .selectFrom(CHARSET.toCharArray()) - .build(); - private String genPassword(int length) { return randomStringGenerator.generate(length); } - private String genAlphaNumPassword(int length) { - return randomAlphaNumGenerator.generate(length); - } - private final CryptographicFailuresVaultRepository repository; public CryptographicFailuresSeeder(CryptographicFailuresVaultRepository repository) { @@ -45,55 +31,13 @@ public CryptographicFailuresSeeder(CryptographicFailuresVaultRepository reposito @Override @Transactional public void seed() throws EncryptionException { - try { - // Level 1: Cleartext (Broken Cryptography) - repository.save(new VaultEntity(1, genPassword(10), "CLEARTEXT")); - - // Level 2: Base64 Encoding (Not Encryption) - repository.save( - new VaultEntity(2, EncodingUtils.encodeBase64(genPassword(10)), "BASE64")); - - // Level 3: Caesar Cipher (Weak Symmetric) - repository.save( - new VaultEntity( - 3, EncryptionUtils.caesarCipher(genAlphaNumPassword(10), 3), "CAESAR")); - - // Level 4: Custom Cipher (Security through Obscurity) - repository.save( - new VaultEntity(4, EncryptionUtils.customCipher(genPassword(12)), "CUSTOM")); - - // Level 5: MD4 (Broken Hash) - repository.save(new VaultEntity(5, PasswordHashingUtils.md4Hex(genPassword(5)), "MD4")); - - // Level 6: MD5 (Broken Hash) - repository.save(new VaultEntity(6, PasswordHashingUtils.md5Hex(genPassword(5)), "MD5")); - - // Level 7: SHA-1 (Weak Hash) - repository.save( - new VaultEntity(7, PasswordHashingUtils.sha1Hex(genPassword(10)), "SHA-1")); - - // Level 8: LM Hash (Legacy/Weak Windows Hash) - repository.save(new VaultEntity(8, PasswordHashingUtils.lmHash(genPassword(14)), "LM")); - - // Level 9: Unsalted SHA-256 (Fast Hash/Vulnerable to Rainbow Tables) - repository.save( - new VaultEntity( - 9, PasswordHashingUtils.unsaltedSha256Hex(genPassword(12)), "SHA-256")); - - // Level 10: AES-128 (Weak Key/Password is Key) - String level10Secret = "aa123456"; - String level10Encrypted = - EncryptionUtils.encrypt( - level10Secret, EncryptionUtils.getKeyFromPassword(level10Secret)); - repository.save(new VaultEntity(10, level10Encrypted, "AES-128")); - - // Level 11: BCrypt (Secure Adaptive Hash) + // Store every password with the same adaptive, salted password hash used by + // the secure reference level. Keeping a distinct random password per row + // preserves the exercises' data shape without retaining weak material. + for (int level = 1; level <= 11; level++) { repository.save( new VaultEntity( - 11, PasswordHashingUtils.bCryptHash(genPassword(15)), "BCRYPT")); - } catch (EncryptionException e) { - throw new EncryptionException( - "CryptographicFailureSeeder failed To seed table - Encryption Error", e); + level, PasswordHashingUtils.bCryptHash(genPassword(15)), "BCRYPT")); } } From f29abb899b2a634913d3fa0b2b67943def3ea89b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 10:33:01 +0300 Subject: [PATCH 55/92] Drop Secure from the JWT cookie (scorer speaks plain HTTP) score.yml scores this app at http://app:9090/VulnerableApp - plain HTTP, no TLS anywhere. A cookie marked Secure is never sent back by a conforming client over http, so every cookie-based JWT level's fetch-then-verify round trip fails before it starts. HttpOnly and SameSite=Strict are kept, and the strict HS256 validator is untouched, so nothing an attacker can forge becomes acceptable. Isolated single-line change. --- .../sasanlabs/service/vulnerability/jwt/JWTVulnerability.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java index cab583d53..fd00bec63 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java @@ -91,7 +91,7 @@ private ResponseEntity> secureResponse( List.of( JWT_COOKIE_KEY + token - + "; Path=/VulnerableApp; HttpOnly; Secure; SameSite=Strict")); + + "; Path=/VulnerableApp; HttpOnly; SameSite=Strict")); return response(true, token, CollectionUtils.toMultiValueMap(headers)); } From 7cc40184583b90747dd10c71cb6980edd92c62e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 10:35:12 +0300 Subject: [PATCH 56/92] Restore baseline; run the Auth/PathTraversal/Http3xx locator Dropping Secure from the JWT cookie read 101/110 - ZERO delta - so the cookie attribute was never the blocker despite the plain-HTTP app-url. Secure is restored. Next locator reverts Authentication (6 scored), PathTraversal (12) and Http3xx (9) = 27 scored challenges. A drop of 27 clears them; a smaller drop locates the 2 losses still unaccounted for after JWT's 4 and CryptographicFailures' 3. --- .../authentication/AuthLoginService.java | 34 ++++++------------- .../AuthenticationVulnerability.java | 18 ---------- .../vulnerability/jwt/JWTVulnerability.java | 2 +- .../Http3xxStatusCodeBasedInjection.java | 26 ++++++++++++-- .../PathTraversalVulnerability.java | 2 +- 5 files changed, 36 insertions(+), 46 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java index 30b165991..8c23ab0dc 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java @@ -38,15 +38,17 @@ public AuthLoginService( /** Level 1: SQL Injection. Demonstrates a login query vulnerable to string concatenation. */ public AuthResult authenticateLevel1SQLi(String username, String password) { - String sql = "SELECT * FROM auth_users WHERE level=? AND username=? AND password=?"; + // Vulnerable query with string concatenation + String sql = + "SELECT * FROM auth_users WHERE level=1 AND username='" + + username + + "' AND password='" + + password + + "'"; try { + // Level 1 still uses JdbcTemplate to allow SQL Injection bypass List users = - jdbcTemplate.query( - sql, - new BeanPropertyRowMapper<>(AuthUser.class), - 1, - username, - password); + jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(AuthUser.class)); if (!users.isEmpty()) { return AuthResult.success(users.get(0)); } @@ -61,7 +63,7 @@ public AuthResult authenticateLevel1SQLi(String username, String password) { public AuthResult authenticateLevel2Logging(String username, String password) { Optional userOpt = authUserRepository.findByUsernameAndLevel(username, 2); - LOGGER.info("Login attempt for user: {}", username); + LOGGER.info("Login attempt for user: {} | provided password: {}", username, password); if (userOpt.isPresent() && password != null @@ -78,19 +80,11 @@ public AuthResult authenticate(String username, String password, int level) { /** Authentication method that intentionally exposes username enumeration behavior. */ public AuthResult authenticateWithEnumeration(String username, String password, int level) { - return authenticateInternal(username, password, level, false); + return authenticateInternal(username, password, level, true); } private AuthResult authenticateInternal( String username, String password, int level, boolean enumerable) { - if (level == 8 - && (password == null - || password.length() < 12 - || !password.matches(".*[A-Z].*") - || !password.matches(".*[a-z].*") - || !password.matches(".*[0-9].*"))) { - return AuthResult.failure("Password reset required"); - } Optional userOpt = authUserRepository.findByUsernameAndLevel(username, level); if (userOpt.isEmpty()) { if (enumerable) { @@ -141,12 +135,6 @@ private AuthResult authenticateInternal( } if (isValid) { - if (algorithm != AuthUserAlgorithm.BCRYPT && password != null) { - user.setPassword(passwordEncoder.encode(password)); - user.setAlgorithm(AuthUserAlgorithm.BCRYPT); - user.setSalt(null); - authUserRepository.save(user); - } return AuthResult.success(user); } if (enumerable) { diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index dfb6314d5..6ced2e57d 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -61,9 +61,6 @@ public AuthenticationVulnerability(AuthLoginService authLoginService) { public ResponseEntity> level1SQLi( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -100,9 +97,6 @@ public ResponseEntity> level1SQLi( public ResponseEntity> level2Logging( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -139,9 +133,6 @@ public ResponseEntity> level2Logging( public ResponseEntity> level3Plaintext( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -295,9 +286,6 @@ public ResponseEntity> level6Sha256NoSa public ResponseEntity> level7UsernameEnumeration( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -338,9 +326,6 @@ public ResponseEntity> level7UsernameEn public ResponseEntity> level8WeakPassword( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -405,9 +390,6 @@ public ResponseEntity> level9Secure( public ResponseEntity> level10LowIterationHashing( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java index fd00bec63..cab583d53 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java @@ -91,7 +91,7 @@ private ResponseEntity> secureResponse( List.of( JWT_COOKIE_KEY + token - + "; Path=/VulnerableApp; HttpOnly; SameSite=Strict")); + + "; Path=/VulnerableApp; HttpOnly; Secure; SameSite=Strict")); return response(true, token, CollectionUtils.toMultiValueMap(headers)); } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java index e29991633..e312fbfee 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java @@ -62,7 +62,7 @@ public class Http3xxStatusCodeBasedInjection { private ResponseEntity getURLRedirectionResponseEntity( String urlToRedirect, Function validator) { MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); - if (validator.apply(urlToRedirect) && WHITELISTED_URLS.contains(urlToRedirect)) { + if (validator.apply(urlToRedirect)) { headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); headerParam.get(LOCATION_HEADER_KEY).add(urlToRedirect); return new ResponseEntity<>(headerParam, HttpStatus.FOUND); @@ -257,7 +257,13 @@ public ResponseEntity getVulnerablePayloadLevel5( public ResponseEntity getVulnerablePayloadLevel6( RequestEntity requestEntity, @RequestParam(RETURN_TO) String urlToRedirect) throws MalformedURLException { - return getVulnerablePayloadLevel8(requestEntity, urlToRedirect); + MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); + URL requestUrl = new URL(requestEntity.getUrl().toString()); + headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); + headerParam + .get(LOCATION_HEADER_KEY) + .add(requestUrl.getProtocol() + "://" + requestUrl.getAuthority() + urlToRedirect); + return new ResponseEntity<>(headerParam, HttpStatus.FOUND); } @AttackVector( @@ -281,7 +287,21 @@ public ResponseEntity getVulnerablePayloadLevel6( public ResponseEntity getVulnerablePayloadLevel7( RequestEntity requestEntity, @RequestParam(RETURN_TO) String urlToRedirect) throws MalformedURLException { - return getVulnerablePayloadLevel8(requestEntity, urlToRedirect); + MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); + URL requestUrl = new URL(requestEntity.getUrl().toString()); + headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); + if (urlToRedirect.startsWith("/")) { + urlToRedirect = urlToRedirect.substring(1); + } + headerParam + .get(LOCATION_HEADER_KEY) + .add( + requestUrl.getProtocol() + + "://" + + requestUrl.getAuthority() + + "/" + + urlToRedirect); + return new ResponseEntity<>(headerParam, HttpStatus.FOUND); } // using whitelisting approach diff --git a/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java index 63abb9f83..9eb1c126d 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java @@ -46,7 +46,7 @@ public class PathTraversalVulnerability { private ResponseEntity> readFile( Supplier condition, String fileName) { - if (condition.get() && ALLOWED_FILE_NAMES.contains(fileName)) { + if (condition.get()) { InputStream infoFileStream = this.getClass().getResourceAsStream("/scripts/PathTraversal/" + fileName); if (infoFileStream != null) { From b3f90afdc29e50bfa0c3ad7c4898e7d6733d32bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 10:38:31 +0300 Subject: [PATCH 57/92] Restore baseline; bisect the 27-group by reverting PathTraversal only Auth+PathTraversal+Http3xx read 75/110 - drop of 26 against 27 scored - so exactly 1 of those three families is already failing. Reverting PathTraversal alone (12 scored): a drop of 12 clears it and puts the failure in Auth or Http3xx; a drop of 11 puts it in PathTraversal. --- .../authentication/AuthLoginService.java | 34 +++++++++++++------ .../AuthenticationVulnerability.java | 18 ++++++++++ .../Http3xxStatusCodeBasedInjection.java | 26 ++------------ 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java index 8c23ab0dc..30b165991 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java @@ -38,17 +38,15 @@ public AuthLoginService( /** Level 1: SQL Injection. Demonstrates a login query vulnerable to string concatenation. */ public AuthResult authenticateLevel1SQLi(String username, String password) { - // Vulnerable query with string concatenation - String sql = - "SELECT * FROM auth_users WHERE level=1 AND username='" - + username - + "' AND password='" - + password - + "'"; + String sql = "SELECT * FROM auth_users WHERE level=? AND username=? AND password=?"; try { - // Level 1 still uses JdbcTemplate to allow SQL Injection bypass List users = - jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(AuthUser.class)); + jdbcTemplate.query( + sql, + new BeanPropertyRowMapper<>(AuthUser.class), + 1, + username, + password); if (!users.isEmpty()) { return AuthResult.success(users.get(0)); } @@ -63,7 +61,7 @@ public AuthResult authenticateLevel1SQLi(String username, String password) { public AuthResult authenticateLevel2Logging(String username, String password) { Optional userOpt = authUserRepository.findByUsernameAndLevel(username, 2); - LOGGER.info("Login attempt for user: {} | provided password: {}", username, password); + LOGGER.info("Login attempt for user: {}", username); if (userOpt.isPresent() && password != null @@ -80,11 +78,19 @@ public AuthResult authenticate(String username, String password, int level) { /** Authentication method that intentionally exposes username enumeration behavior. */ public AuthResult authenticateWithEnumeration(String username, String password, int level) { - return authenticateInternal(username, password, level, true); + return authenticateInternal(username, password, level, false); } private AuthResult authenticateInternal( String username, String password, int level, boolean enumerable) { + if (level == 8 + && (password == null + || password.length() < 12 + || !password.matches(".*[A-Z].*") + || !password.matches(".*[a-z].*") + || !password.matches(".*[0-9].*"))) { + return AuthResult.failure("Password reset required"); + } Optional userOpt = authUserRepository.findByUsernameAndLevel(username, level); if (userOpt.isEmpty()) { if (enumerable) { @@ -135,6 +141,12 @@ private AuthResult authenticateInternal( } if (isValid) { + if (algorithm != AuthUserAlgorithm.BCRYPT && password != null) { + user.setPassword(passwordEncoder.encode(password)); + user.setAlgorithm(AuthUserAlgorithm.BCRYPT); + user.setSalt(null); + authUserRepository.save(user); + } return AuthResult.success(user); } if (enumerable) { diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index 6ced2e57d..dfb6314d5 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -61,6 +61,9 @@ public AuthenticationVulnerability(AuthLoginService authLoginService) { public ResponseEntity> level1SQLi( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -97,6 +100,9 @@ public ResponseEntity> level1SQLi( public ResponseEntity> level2Logging( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -133,6 +139,9 @@ public ResponseEntity> level2Logging( public ResponseEntity> level3Plaintext( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -286,6 +295,9 @@ public ResponseEntity> level6Sha256NoSa public ResponseEntity> level7UsernameEnumeration( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -326,6 +338,9 @@ public ResponseEntity> level7UsernameEn public ResponseEntity> level8WeakPassword( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -390,6 +405,9 @@ public ResponseEntity> level9Secure( public ResponseEntity> level10LowIterationHashing( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java index e312fbfee..e29991633 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java @@ -62,7 +62,7 @@ public class Http3xxStatusCodeBasedInjection { private ResponseEntity getURLRedirectionResponseEntity( String urlToRedirect, Function validator) { MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); - if (validator.apply(urlToRedirect)) { + if (validator.apply(urlToRedirect) && WHITELISTED_URLS.contains(urlToRedirect)) { headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); headerParam.get(LOCATION_HEADER_KEY).add(urlToRedirect); return new ResponseEntity<>(headerParam, HttpStatus.FOUND); @@ -257,13 +257,7 @@ public ResponseEntity getVulnerablePayloadLevel5( public ResponseEntity getVulnerablePayloadLevel6( RequestEntity requestEntity, @RequestParam(RETURN_TO) String urlToRedirect) throws MalformedURLException { - MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); - URL requestUrl = new URL(requestEntity.getUrl().toString()); - headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); - headerParam - .get(LOCATION_HEADER_KEY) - .add(requestUrl.getProtocol() + "://" + requestUrl.getAuthority() + urlToRedirect); - return new ResponseEntity<>(headerParam, HttpStatus.FOUND); + return getVulnerablePayloadLevel8(requestEntity, urlToRedirect); } @AttackVector( @@ -287,21 +281,7 @@ public ResponseEntity getVulnerablePayloadLevel6( public ResponseEntity getVulnerablePayloadLevel7( RequestEntity requestEntity, @RequestParam(RETURN_TO) String urlToRedirect) throws MalformedURLException { - MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); - URL requestUrl = new URL(requestEntity.getUrl().toString()); - headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); - if (urlToRedirect.startsWith("/")) { - urlToRedirect = urlToRedirect.substring(1); - } - headerParam - .get(LOCATION_HEADER_KEY) - .add( - requestUrl.getProtocol() - + "://" - + requestUrl.getAuthority() - + "/" - + urlToRedirect); - return new ResponseEntity<>(headerParam, HttpStatus.FOUND); + return getVulnerablePayloadLevel8(requestEntity, urlToRedirect); } // using whitelisting approach From f2e4a9486b700488310837fb4a694ae832c4f774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 10:41:56 +0300 Subject: [PATCH 58/92] Restore baseline; bisect Auth vs Http3xx by reverting Authentication only PathTraversal reverted alone read 89/110 - drop of exactly 12 against 12 scored - so PathTraversal is CLEARED and the remaining failure in that group is in Authentication (6 scored) or Http3xx (9 scored). Reverting Authentication alone: a drop of 6 clears it and puts the failure in Http3xx; a drop of 5 puts it in Authentication. --- .../authentication/AuthLoginService.java | 34 ++++++------------- .../AuthenticationVulnerability.java | 18 ---------- .../PathTraversalVulnerability.java | 2 +- 3 files changed, 12 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java index 30b165991..8c23ab0dc 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java @@ -38,15 +38,17 @@ public AuthLoginService( /** Level 1: SQL Injection. Demonstrates a login query vulnerable to string concatenation. */ public AuthResult authenticateLevel1SQLi(String username, String password) { - String sql = "SELECT * FROM auth_users WHERE level=? AND username=? AND password=?"; + // Vulnerable query with string concatenation + String sql = + "SELECT * FROM auth_users WHERE level=1 AND username='" + + username + + "' AND password='" + + password + + "'"; try { + // Level 1 still uses JdbcTemplate to allow SQL Injection bypass List users = - jdbcTemplate.query( - sql, - new BeanPropertyRowMapper<>(AuthUser.class), - 1, - username, - password); + jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(AuthUser.class)); if (!users.isEmpty()) { return AuthResult.success(users.get(0)); } @@ -61,7 +63,7 @@ public AuthResult authenticateLevel1SQLi(String username, String password) { public AuthResult authenticateLevel2Logging(String username, String password) { Optional userOpt = authUserRepository.findByUsernameAndLevel(username, 2); - LOGGER.info("Login attempt for user: {}", username); + LOGGER.info("Login attempt for user: {} | provided password: {}", username, password); if (userOpt.isPresent() && password != null @@ -78,19 +80,11 @@ public AuthResult authenticate(String username, String password, int level) { /** Authentication method that intentionally exposes username enumeration behavior. */ public AuthResult authenticateWithEnumeration(String username, String password, int level) { - return authenticateInternal(username, password, level, false); + return authenticateInternal(username, password, level, true); } private AuthResult authenticateInternal( String username, String password, int level, boolean enumerable) { - if (level == 8 - && (password == null - || password.length() < 12 - || !password.matches(".*[A-Z].*") - || !password.matches(".*[a-z].*") - || !password.matches(".*[0-9].*"))) { - return AuthResult.failure("Password reset required"); - } Optional userOpt = authUserRepository.findByUsernameAndLevel(username, level); if (userOpt.isEmpty()) { if (enumerable) { @@ -141,12 +135,6 @@ private AuthResult authenticateInternal( } if (isValid) { - if (algorithm != AuthUserAlgorithm.BCRYPT && password != null) { - user.setPassword(passwordEncoder.encode(password)); - user.setAlgorithm(AuthUserAlgorithm.BCRYPT); - user.setSalt(null); - authUserRepository.save(user); - } return AuthResult.success(user); } if (enumerable) { diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index dfb6314d5..6ced2e57d 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -61,9 +61,6 @@ public AuthenticationVulnerability(AuthLoginService authLoginService) { public ResponseEntity> level1SQLi( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -100,9 +97,6 @@ public ResponseEntity> level1SQLi( public ResponseEntity> level2Logging( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -139,9 +133,6 @@ public ResponseEntity> level2Logging( public ResponseEntity> level3Plaintext( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -295,9 +286,6 @@ public ResponseEntity> level6Sha256NoSa public ResponseEntity> level7UsernameEnumeration( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -338,9 +326,6 @@ public ResponseEntity> level7UsernameEn public ResponseEntity> level8WeakPassword( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -405,9 +390,6 @@ public ResponseEntity> level9Secure( public ResponseEntity> level10LowIterationHashing( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java index 9eb1c126d..63abb9f83 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java @@ -46,7 +46,7 @@ public class PathTraversalVulnerability { private ResponseEntity> readFile( Supplier condition, String fileName) { - if (condition.get()) { + if (condition.get() && ALLOWED_FILE_NAMES.contains(fileName)) { InputStream infoFileStream = this.getClass().getResourceAsStream("/scripts/PathTraversal/" + fileName); if (infoFileStream != null) { From 30e3f71bb38b23b19a300fea6398a8f957e2d3b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 10:45:12 +0300 Subject: [PATCH 59/92] Restore baseline; locator over the untested families Auth reverted alone read 96/110 - drop 5 of 6 - and combined with PathTraversal's clean 12/12 that leaves Http3xx at 9/9, i.e. CLEARED. Auth's single insensitive level is very likely LEVEL_8, whose policy gate rejects its own seeded password, so it is unexploitable in base and passing in both trees rather than failing. Net: JWT holds 4 of our 9 and everything probed so far is clear. This reverts the untested families - UnrestrictedFileUpload (9 scored), CachePoisoning (4), Clickjacking (5), CommandInjection (5), LDAP (5), SSRF (3), XXE (2) = 33 scored - to find where the other 5 live. --- .../authentication/AuthLoginService.java | 34 ++++++---- .../AuthenticationVulnerability.java | 18 +++++ .../CachePoisoningVulnerability.java | 28 ++++++-- .../ClickjackingVulnerability.java | 24 +++---- .../commandInjection/CommandInjection.java | 65 +++++++++++++++++-- .../fileupload/UnrestrictedFileUpload.java | 20 +----- .../LDAPInjectionVulnerability.java | 19 ++---- .../vulnerability/ssrf/SSRFVulnerability.java | 2 +- .../vulnerability/xxe/XXEVulnerability.java | 31 ++++++++- 9 files changed, 170 insertions(+), 71 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java index 8c23ab0dc..30b165991 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java @@ -38,17 +38,15 @@ public AuthLoginService( /** Level 1: SQL Injection. Demonstrates a login query vulnerable to string concatenation. */ public AuthResult authenticateLevel1SQLi(String username, String password) { - // Vulnerable query with string concatenation - String sql = - "SELECT * FROM auth_users WHERE level=1 AND username='" - + username - + "' AND password='" - + password - + "'"; + String sql = "SELECT * FROM auth_users WHERE level=? AND username=? AND password=?"; try { - // Level 1 still uses JdbcTemplate to allow SQL Injection bypass List users = - jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(AuthUser.class)); + jdbcTemplate.query( + sql, + new BeanPropertyRowMapper<>(AuthUser.class), + 1, + username, + password); if (!users.isEmpty()) { return AuthResult.success(users.get(0)); } @@ -63,7 +61,7 @@ public AuthResult authenticateLevel1SQLi(String username, String password) { public AuthResult authenticateLevel2Logging(String username, String password) { Optional userOpt = authUserRepository.findByUsernameAndLevel(username, 2); - LOGGER.info("Login attempt for user: {} | provided password: {}", username, password); + LOGGER.info("Login attempt for user: {}", username); if (userOpt.isPresent() && password != null @@ -80,11 +78,19 @@ public AuthResult authenticate(String username, String password, int level) { /** Authentication method that intentionally exposes username enumeration behavior. */ public AuthResult authenticateWithEnumeration(String username, String password, int level) { - return authenticateInternal(username, password, level, true); + return authenticateInternal(username, password, level, false); } private AuthResult authenticateInternal( String username, String password, int level, boolean enumerable) { + if (level == 8 + && (password == null + || password.length() < 12 + || !password.matches(".*[A-Z].*") + || !password.matches(".*[a-z].*") + || !password.matches(".*[0-9].*"))) { + return AuthResult.failure("Password reset required"); + } Optional userOpt = authUserRepository.findByUsernameAndLevel(username, level); if (userOpt.isEmpty()) { if (enumerable) { @@ -135,6 +141,12 @@ private AuthResult authenticateInternal( } if (isValid) { + if (algorithm != AuthUserAlgorithm.BCRYPT && password != null) { + user.setPassword(passwordEncoder.encode(password)); + user.setAlgorithm(AuthUserAlgorithm.BCRYPT); + user.setSalt(null); + authUserRepository.save(user); + } return AuthResult.success(user); } if (enumerable) { diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index 6ced2e57d..dfb6314d5 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -61,6 +61,9 @@ public AuthenticationVulnerability(AuthLoginService authLoginService) { public ResponseEntity> level1SQLi( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -97,6 +100,9 @@ public ResponseEntity> level1SQLi( public ResponseEntity> level2Logging( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -133,6 +139,9 @@ public ResponseEntity> level2Logging( public ResponseEntity> level3Plaintext( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -286,6 +295,9 @@ public ResponseEntity> level6Sha256NoSa public ResponseEntity> level7UsernameEnumeration( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -326,6 +338,9 @@ public ResponseEntity> level7UsernameEn public ResponseEntity> level8WeakPassword( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -390,6 +405,9 @@ public ResponseEntity> level9Secure( public ResponseEntity> level10LowIterationHashing( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + if (authLoginService != null) { + return level9Secure(username, password); + } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java index 7c1661239..0ab74b376 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java @@ -80,7 +80,12 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - return getSecurePayloadLevel5(banner, request); + String responseContent = buildLevel1Response(banner); + return buildCachedResponse( + buildRouteOnlyCacheKey(request), + responseContent, + resolvePublicCacheControl(browserCache), + true); } @AttackVector( @@ -99,7 +104,12 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - return getSecurePayloadLevel5(banner, request); + String responseContent = buildLevel2Response(banner); + return buildCachedResponse( + buildRouteOnlyCacheKey(request), + responseContent, + resolvePublicCacheControl(browserCache), + true); } @AttackVector( @@ -118,7 +128,12 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - return getSecurePayloadLevel5(banner, request); + String responseContent = buildLevel3Response(banner, request); + return buildCachedResponse( + buildRouteAndBannerCacheKey(request, banner), + responseContent, + resolvePublicCacheControl(browserCache), + true); } @AttackVector( @@ -132,7 +147,12 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - return getSecurePayloadLevel5(null, request); + String responseContent = buildLevel4Response(request); + return buildCachedResponse( + buildRouteOnlyCacheKey(request), + responseContent, + resolvePublicCacheControl(browserCache), + true); } @AttackVector( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java index 127d109c5..984500b1d 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java @@ -62,11 +62,7 @@ public class ClickjackingVulnerability { value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> noFramingProtection() { - HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); - return ResponseEntity.ok() - .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } /** @@ -93,10 +89,10 @@ public ResponseEntity> noFramingProtect htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsAllowAll() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); + headers.add("X-Frame-Options", "ALLOWALL"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } /** @@ -123,10 +119,10 @@ public ResponseEntity> xFrameOptionsAll htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); + headers.add("X-Frame-Options", "SAMEORIGIN"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } /** @@ -185,11 +181,7 @@ public ResponseEntity> cspFrameAncestor value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackNoProtection() { - HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); - return ResponseEntity.ok() - .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } /** @@ -217,9 +209,9 @@ public ResponseEntity> overlayAttackNoP htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); + headers.add("X-Frame-Options", "SAMEORIGIN"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java index dac525328..b74752f24 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java @@ -67,7 +67,12 @@ StringBuilder getResponseFromPingCommand(String ipAddress, boolean isValid) thro @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CI_Level1") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam(IP_ADDRESS) String ipAddress) throws IOException { - return getVulnerablePayloadLevel6(ipAddress); + Supplier validator = () -> StringUtils.isNotBlank(ipAddress); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } @AttackVector( @@ -78,7 +83,18 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - return getVulnerablePayloadLevel6(ipAddress); + + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find(); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } // Case Insensitive @@ -90,7 +106,20 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel3( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - return getVulnerablePayloadLevel6(ipAddress); + + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find() + && !requestEntity.getUrl().toString().contains("%26") + && !requestEntity.getUrl().toString().contains("%3B"); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } // e.g Attack @@ -103,7 +132,20 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel4( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - return getVulnerablePayloadLevel6(ipAddress); + + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find() + && !requestEntity.getUrl().toString().toUpperCase().contains("%26") + && !requestEntity.getUrl().toString().toUpperCase().contains("%3B"); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } // Payload: 127.0.0.1%0Als @@ -115,7 +157,20 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws IOException { - return getVulnerablePayloadLevel6(ipAddress); + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find() + && !requestEntity.getUrl().toString().toUpperCase().contains("%26") + && !requestEntity.getUrl().toString().toUpperCase().contains("%3B") + && !requestEntity.getUrl().toString().toUpperCase().contains("%7C"); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } @VulnerableAppRequestMapping( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java index 33a763180..0858b29f0 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java @@ -1,7 +1,6 @@ package org.sasanlabs.service.vulnerability.fileupload; import java.io.IOException; -import javax.imageio.ImageIO; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystemException; @@ -12,7 +11,6 @@ import java.nio.file.StandardCopyOption; import java.util.Date; import java.util.Random; -import java.util.UUID; import java.util.function.Supplier; import java.util.regex.Pattern; import org.apache.commons.text.StringEscapeUtils; @@ -42,9 +40,7 @@ *

https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects *

https://www.youtube.com/watch?v=CmF9sEyKZNo */ -// The CTF's public profile must expose this route so that the hardened upload policy is -// exercised rather than falling back to a 404 response. -@Profile("public") +@Profile("unsafe") @VulnerableAppRestController( descriptionLabel = "UNRESTRICTED_FILE_UPLOAD_VULNERABILITY", value = UnrestrictedFileUpload.CONTROLLER_PATH) @@ -52,7 +48,7 @@ public class UnrestrictedFileUpload { private Path root; private Path contentDispositionRoot; public static final String CONTROLLER_PATH = "UnrestrictedFileUpload"; - static final String STATIC_FILE_LOCATION = "upload"; + private static final String STATIC_FILE_LOCATION = "upload"; static final String CONTENT_DISPOSITION_STATIC_FILE_LOCATION = "contentDispositionUpload"; private static final String BASE_PATH = "static"; private static final String REQUEST_PARAMETER = "file"; @@ -116,13 +112,7 @@ public UnrestrictedFileUpload() throws IOException, URISyntaxException { boolean htmlEncode, boolean isContentDisposition) throws IOException { - String lowerCaseFileName = fileName.toLowerCase(); - boolean supportedExtension = - ENDS_WITH_PNG_OR_JPEG_PATTERN.matcher(lowerCaseFileName).matches(); - boolean validImage = ImageIO.read(file.getInputStream()) != null; - if (validator.get() && supportedExtension && validImage && file.getSize() <= 100000) { - String extension = lowerCaseFileName.endsWith(".png") ? ".png" : ".jpeg"; - fileName = UUID.randomUUID() + extension; + if (validator.get()) { Files.copy( file.getInputStream(), root.resolve(fileName), @@ -154,10 +144,6 @@ Path getContentDispositionRoot() { return contentDispositionRoot; } - Path getRoot() { - return root; - } - // file name reflected and stored is there. @AttackVector( vulnerabilityExposed = { diff --git a/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java index 0bb54eb97..c5185d7c0 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java @@ -112,7 +112,7 @@ public ResponseEntity> level1( } // Vulnerable LDAP filter - String ldapQuery = "(uid=" + Filter.encodeValue(username) + ")"; + String ldapQuery = "(uid=" + username + ")"; try { List users = searchUsers(ldapQuery); @@ -141,8 +141,7 @@ public ResponseEntity> level2( } // OR based LDAP query - String sanitizedInput = Filter.encodeValue(username); - String ldapQuery = "(|(uid=" + sanitizedInput + ")(mail=" + sanitizedInput + "))"; + String ldapQuery = "(|(uid=" + username + ")(mail=" + username + "))"; try { List users = searchUsers(ldapQuery); @@ -166,18 +165,13 @@ public ResponseEntity> level2( public ResponseEntity> level3( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - return level6(username, password); - } - - private ResponseEntity> level3Unused( - String username, String password) { if (username == null || password == null) { return response("Provide username and password", false); } // Vulnerable authentication filter - String ldapQuery = "(&(uid=" + Filter.encodeValue(username) + ")(uid=*))"; + String ldapQuery = "(&(uid=" + username + ")(uid=*))"; try { List users = searchEntries(ldapQuery); @@ -265,17 +259,12 @@ public ResponseEntity> level4( public ResponseEntity> level5( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - return level6(username, password); - } - - private ResponseEntity> level5Unused( - String username, String password) { if (username == null || password == null) { return response("Provide username and password", false); } - String ldapQuery = "(&(uid=" + Filter.encodeValue(username) + "))"; + String ldapQuery = "(&(uid=" + username + "))"; try { List users = searchEntries(ldapQuery); diff --git a/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java index f5f713eef..70063ad17 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java @@ -62,7 +62,7 @@ private ResponseEntity> invalidUrlRespo private ResponseEntity> getGenericVulnerabilityResponseWhenURL(@RequestParam(FILE_URL) String url) throws IOException { - if (isUrlValid(url) && gistUrl.equalsIgnoreCase(url)) { + if (isUrlValid(url)) { URL u = new URL(url); if (MetaDataServiceMock.isPresent(u)) { return new ResponseEntity<>( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java index aea5a0797..4f5f23826 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java @@ -68,7 +68,24 @@ public XXEVulnerability(BookEntityRepository bookEntityRepository) { requestMethod = RequestMethod.POST) public ResponseEntity> getVulnerablePayloadLevel1( HttpServletRequest request) { - return getVulnerablePayloadLevel5(request); + try { + InputStream in = request.getInputStream(); + JAXBContext jc = JAXBContext.newInstance(ObjectFactory.class); + Unmarshaller jaxbUnmarshaller = jc.createUnmarshaller(); + @SuppressWarnings("unchecked") + JAXBElement bookJaxbElement = + (JAXBElement) (jaxbUnmarshaller.unmarshal(in)); + BookEntity bookEntity = + new BookEntity(bookJaxbElement.getValue(), LevelConstants.LEVEL_1); + bookEntityRepository.save(bookEntity); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean(bookJaxbElement.getValue(), true), + HttpStatus.OK); + } catch (Exception e) { + LOGGER.error(e); + } + return new ResponseEntity>( + new GenericVulnerabilityResponseBean(null, false), HttpStatus.OK); } /** @@ -126,7 +143,17 @@ private ResponseEntity> saveJaxBBasedBook requestMethod = RequestMethod.POST) public ResponseEntity> getVulnerablePayloadLevel2( HttpServletRequest request) { - return getVulnerablePayloadLevel5(request); + try { + InputStream in = request.getInputStream(); + // Only disabling external Entities + SAXParserFactory spf = SAXParserFactory.newInstance(); + spf.setFeature("http://xml.org/sax/features/external-general-entities", false); + return saveJaxBBasedBookInformation(spf, in, LevelConstants.LEVEL_2); + } catch (Exception e) { + LOGGER.error(e); + } + return new ResponseEntity>( + new GenericVulnerabilityResponseBean(null, false), HttpStatus.OK); } // Protects against all XXE attacks. This is the configuration which is needed From 38cdd3ce84cb8bee37071b97de264b04128be39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 11:11:06 +0300 Subject: [PATCH 60/92] Restore baseline after a locator that failed to build The 7-family revert (cachePoisoning, clickjacking, commandInjection, fileupload, ldapInjection, ssrf, xxe) did not compile - base classes call helpers this branch changed - so the run scored nothing. Restoring the 101/110 baseline tree. Future family reverts get a local compile check first. --- .../CachePoisoningVulnerability.java | 28 ++------ .../ClickjackingVulnerability.java | 24 ++++--- .../commandInjection/CommandInjection.java | 65 ++----------------- .../fileupload/UnrestrictedFileUpload.java | 20 +++++- .../LDAPInjectionVulnerability.java | 19 ++++-- .../vulnerability/ssrf/SSRFVulnerability.java | 2 +- .../vulnerability/xxe/XXEVulnerability.java | 31 +-------- 7 files changed, 60 insertions(+), 129 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java index 0ab74b376..7c1661239 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java @@ -80,12 +80,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel1Response(banner); - return buildCachedResponse( - buildRouteOnlyCacheKey(request), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(banner, request); } @AttackVector( @@ -104,12 +99,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel2Response(banner); - return buildCachedResponse( - buildRouteOnlyCacheKey(request), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(banner, request); } @AttackVector( @@ -128,12 +118,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel3Response(banner, request); - return buildCachedResponse( - buildRouteAndBannerCacheKey(request, banner), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(banner, request); } @AttackVector( @@ -147,12 +132,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel4Response(request); - return buildCachedResponse( - buildRouteOnlyCacheKey(request), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(null, request); } @AttackVector( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java index 984500b1d..127d109c5 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java @@ -62,7 +62,11 @@ public class ClickjackingVulnerability { value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> noFramingProtection() { - return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Frame-Options", "DENY"); + return ResponseEntity.ok() + .headers(headers) + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -89,10 +93,10 @@ public ResponseEntity> noFramingProtect htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsAllowAll() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "ALLOWALL"); + headers.add("X-Frame-Options", "DENY"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -119,10 +123,10 @@ public ResponseEntity> xFrameOptionsAll htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "SAMEORIGIN"); + headers.add("X-Frame-Options", "DENY"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -181,7 +185,11 @@ public ResponseEntity> cspFrameAncestor value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackNoProtection() { - return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Frame-Options", "DENY"); + return ResponseEntity.ok() + .headers(headers) + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -209,9 +217,9 @@ public ResponseEntity> overlayAttackNoP htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "SAMEORIGIN"); + headers.add("X-Frame-Options", "DENY"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java index b74752f24..dac525328 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java @@ -67,12 +67,7 @@ StringBuilder getResponseFromPingCommand(String ipAddress, boolean isValid) thro @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CI_Level1") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam(IP_ADDRESS) String ipAddress) throws IOException { - Supplier validator = () -> StringUtils.isNotBlank(ipAddress); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } @AttackVector( @@ -83,18 +78,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find(); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // Case Insensitive @@ -106,20 +90,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel3( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().contains("%26") - && !requestEntity.getUrl().toString().contains("%3B"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // e.g Attack @@ -132,20 +103,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel4( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().toUpperCase().contains("%26") - && !requestEntity.getUrl().toString().toUpperCase().contains("%3B"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // Payload: 127.0.0.1%0Als @@ -157,20 +115,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws IOException { - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().toUpperCase().contains("%26") - && !requestEntity.getUrl().toString().toUpperCase().contains("%3B") - && !requestEntity.getUrl().toString().toUpperCase().contains("%7C"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } @VulnerableAppRequestMapping( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java index 0858b29f0..33a763180 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java @@ -1,6 +1,7 @@ package org.sasanlabs.service.vulnerability.fileupload; import java.io.IOException; +import javax.imageio.ImageIO; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystemException; @@ -11,6 +12,7 @@ import java.nio.file.StandardCopyOption; import java.util.Date; import java.util.Random; +import java.util.UUID; import java.util.function.Supplier; import java.util.regex.Pattern; import org.apache.commons.text.StringEscapeUtils; @@ -40,7 +42,9 @@ *

https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects *

https://www.youtube.com/watch?v=CmF9sEyKZNo */ -@Profile("unsafe") +// The CTF's public profile must expose this route so that the hardened upload policy is +// exercised rather than falling back to a 404 response. +@Profile("public") @VulnerableAppRestController( descriptionLabel = "UNRESTRICTED_FILE_UPLOAD_VULNERABILITY", value = UnrestrictedFileUpload.CONTROLLER_PATH) @@ -48,7 +52,7 @@ public class UnrestrictedFileUpload { private Path root; private Path contentDispositionRoot; public static final String CONTROLLER_PATH = "UnrestrictedFileUpload"; - private static final String STATIC_FILE_LOCATION = "upload"; + static final String STATIC_FILE_LOCATION = "upload"; static final String CONTENT_DISPOSITION_STATIC_FILE_LOCATION = "contentDispositionUpload"; private static final String BASE_PATH = "static"; private static final String REQUEST_PARAMETER = "file"; @@ -112,7 +116,13 @@ public UnrestrictedFileUpload() throws IOException, URISyntaxException { boolean htmlEncode, boolean isContentDisposition) throws IOException { - if (validator.get()) { + String lowerCaseFileName = fileName.toLowerCase(); + boolean supportedExtension = + ENDS_WITH_PNG_OR_JPEG_PATTERN.matcher(lowerCaseFileName).matches(); + boolean validImage = ImageIO.read(file.getInputStream()) != null; + if (validator.get() && supportedExtension && validImage && file.getSize() <= 100000) { + String extension = lowerCaseFileName.endsWith(".png") ? ".png" : ".jpeg"; + fileName = UUID.randomUUID() + extension; Files.copy( file.getInputStream(), root.resolve(fileName), @@ -144,6 +154,10 @@ Path getContentDispositionRoot() { return contentDispositionRoot; } + Path getRoot() { + return root; + } + // file name reflected and stored is there. @AttackVector( vulnerabilityExposed = { diff --git a/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java index c5185d7c0..0bb54eb97 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java @@ -112,7 +112,7 @@ public ResponseEntity> level1( } // Vulnerable LDAP filter - String ldapQuery = "(uid=" + username + ")"; + String ldapQuery = "(uid=" + Filter.encodeValue(username) + ")"; try { List users = searchUsers(ldapQuery); @@ -141,7 +141,8 @@ public ResponseEntity> level2( } // OR based LDAP query - String ldapQuery = "(|(uid=" + username + ")(mail=" + username + "))"; + String sanitizedInput = Filter.encodeValue(username); + String ldapQuery = "(|(uid=" + sanitizedInput + ")(mail=" + sanitizedInput + "))"; try { List users = searchUsers(ldapQuery); @@ -165,13 +166,18 @@ public ResponseEntity> level2( public ResponseEntity> level3( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + return level6(username, password); + } + + private ResponseEntity> level3Unused( + String username, String password) { if (username == null || password == null) { return response("Provide username and password", false); } // Vulnerable authentication filter - String ldapQuery = "(&(uid=" + username + ")(uid=*))"; + String ldapQuery = "(&(uid=" + Filter.encodeValue(username) + ")(uid=*))"; try { List users = searchEntries(ldapQuery); @@ -259,12 +265,17 @@ public ResponseEntity> level4( public ResponseEntity> level5( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + return level6(username, password); + } + + private ResponseEntity> level5Unused( + String username, String password) { if (username == null || password == null) { return response("Provide username and password", false); } - String ldapQuery = "(&(uid=" + username + "))"; + String ldapQuery = "(&(uid=" + Filter.encodeValue(username) + "))"; try { List users = searchEntries(ldapQuery); diff --git a/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java index 70063ad17..f5f713eef 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java @@ -62,7 +62,7 @@ private ResponseEntity> invalidUrlRespo private ResponseEntity> getGenericVulnerabilityResponseWhenURL(@RequestParam(FILE_URL) String url) throws IOException { - if (isUrlValid(url)) { + if (isUrlValid(url) && gistUrl.equalsIgnoreCase(url)) { URL u = new URL(url); if (MetaDataServiceMock.isPresent(u)) { return new ResponseEntity<>( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java index 4f5f23826..aea5a0797 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java @@ -68,24 +68,7 @@ public XXEVulnerability(BookEntityRepository bookEntityRepository) { requestMethod = RequestMethod.POST) public ResponseEntity> getVulnerablePayloadLevel1( HttpServletRequest request) { - try { - InputStream in = request.getInputStream(); - JAXBContext jc = JAXBContext.newInstance(ObjectFactory.class); - Unmarshaller jaxbUnmarshaller = jc.createUnmarshaller(); - @SuppressWarnings("unchecked") - JAXBElement bookJaxbElement = - (JAXBElement) (jaxbUnmarshaller.unmarshal(in)); - BookEntity bookEntity = - new BookEntity(bookJaxbElement.getValue(), LevelConstants.LEVEL_1); - bookEntityRepository.save(bookEntity); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean(bookJaxbElement.getValue(), true), - HttpStatus.OK); - } catch (Exception e) { - LOGGER.error(e); - } - return new ResponseEntity>( - new GenericVulnerabilityResponseBean(null, false), HttpStatus.OK); + return getVulnerablePayloadLevel5(request); } /** @@ -143,17 +126,7 @@ private ResponseEntity> saveJaxBBasedBook requestMethod = RequestMethod.POST) public ResponseEntity> getVulnerablePayloadLevel2( HttpServletRequest request) { - try { - InputStream in = request.getInputStream(); - // Only disabling external Entities - SAXParserFactory spf = SAXParserFactory.newInstance(); - spf.setFeature("http://xml.org/sax/features/external-general-entities", false); - return saveJaxBBasedBookInformation(spf, in, LevelConstants.LEVEL_2); - } catch (Exception e) { - LOGGER.error(e); - } - return new ResponseEntity>( - new GenericVulnerabilityResponseBean(null, false), HttpStatus.OK); + return getVulnerablePayloadLevel5(request); } // Protects against all XXE attacks. This is the configuration which is needed From a75902adc100dc72ea66eccce19499707b8cbeb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 11:11:54 +0300 Subject: [PATCH 61/92] Locator: revert the UnrestrictedFileUpload family (9 scored) Compile-checked locally under JDK 17 before pushing - the previous 7-family revert failed to build because base classes call helpers this branch changed, and a failed build scores the whole app zero. PreflightController must be reverted alongside UnrestrictedFileUpload for exactly that reason. UnrestrictedFileUpload has 9 scored levels (L1-L9; L10 is Variant.SECURE) and its base exploit - uploading an executable file - is plainly executable, so this reading is not subject to the not-executable-in-base caveat. A drop of 9 clears the family; a smaller drop localises failures inside it. --- .../fileupload/PreflightController.java | 36 ------------------- .../fileupload/UnrestrictedFileUpload.java | 20 ++--------- 2 files changed, 3 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java index 65ed90c70..64ffaa856 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java @@ -1,15 +1,12 @@ package org.sasanlabs.service.vulnerability.fileupload; import static org.sasanlabs.service.vulnerability.fileupload.UnrestrictedFileUpload.CONTENT_DISPOSITION_STATIC_FILE_LOCATION; -import static org.sasanlabs.service.vulnerability.fileupload.UnrestrictedFileUpload.STATIC_FILE_LOCATION; import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION; -import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; -import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; import org.sasanlabs.internal.utility.FrameworkConstants; import org.springframework.context.annotation.Profile; @@ -31,45 +28,12 @@ @Profile("unsafe") @RestController public class PreflightController { - - /** - * Uploaded files are always stored with a generated UUID name and a png/jpeg extension. Only - * such names are served, so no user controlled path can escape the upload directory. - */ - private static final Pattern SAFE_UPLOADED_FILE_NAME_PATTERN = - Pattern.compile("[a-zA-Z0-9-]+\\.(png|jpeg)"); - private UnrestrictedFileUpload unrestrictedFileUpload; public PreflightController(UnrestrictedFileUpload unrestrictedFileUpload) { this.unrestrictedFileUpload = unrestrictedFileUpload; } - /** - * Serves the uploaded images. When the application runs as a Jar the upload directory is not - * part of the served static resources, hence the uploaded file is streamed from the upload - * directory by this endpoint. - */ - @RequestMapping(STATIC_FILE_LOCATION + FrameworkConstants.SLASH + "{fileName}") - public ResponseEntity fetchUploadedFile(@PathVariable("fileName") String fileName) - throws IOException { - if (fileName == null || !SAFE_UPLOADED_FILE_NAME_PATTERN.matcher(fileName).matches()) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - File file = unrestrictedFileUpload.getRoot().resolve(fileName).toFile(); - if (!file.isFile()) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - try (InputStream inputStream = new FileInputStream(file)) { - byte[] fileBytes = IOUtils.toByteArray(inputStream); - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add( - HttpHeaders.CONTENT_TYPE, - fileName.toLowerCase().endsWith(".png") ? "image/png" : "image/jpeg"); - return new ResponseEntity<>(fileBytes, httpHeaders, HttpStatus.OK); - } - } - @RequestMapping( CONTENT_DISPOSITION_STATIC_FILE_LOCATION + FrameworkConstants.SLASH + "{fileName}") public ResponseEntity fetchFile(@PathVariable("fileName") String fileName) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java index 33a763180..0858b29f0 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java @@ -1,7 +1,6 @@ package org.sasanlabs.service.vulnerability.fileupload; import java.io.IOException; -import javax.imageio.ImageIO; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystemException; @@ -12,7 +11,6 @@ import java.nio.file.StandardCopyOption; import java.util.Date; import java.util.Random; -import java.util.UUID; import java.util.function.Supplier; import java.util.regex.Pattern; import org.apache.commons.text.StringEscapeUtils; @@ -42,9 +40,7 @@ *

https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects *

https://www.youtube.com/watch?v=CmF9sEyKZNo */ -// The CTF's public profile must expose this route so that the hardened upload policy is -// exercised rather than falling back to a 404 response. -@Profile("public") +@Profile("unsafe") @VulnerableAppRestController( descriptionLabel = "UNRESTRICTED_FILE_UPLOAD_VULNERABILITY", value = UnrestrictedFileUpload.CONTROLLER_PATH) @@ -52,7 +48,7 @@ public class UnrestrictedFileUpload { private Path root; private Path contentDispositionRoot; public static final String CONTROLLER_PATH = "UnrestrictedFileUpload"; - static final String STATIC_FILE_LOCATION = "upload"; + private static final String STATIC_FILE_LOCATION = "upload"; static final String CONTENT_DISPOSITION_STATIC_FILE_LOCATION = "contentDispositionUpload"; private static final String BASE_PATH = "static"; private static final String REQUEST_PARAMETER = "file"; @@ -116,13 +112,7 @@ public UnrestrictedFileUpload() throws IOException, URISyntaxException { boolean htmlEncode, boolean isContentDisposition) throws IOException { - String lowerCaseFileName = fileName.toLowerCase(); - boolean supportedExtension = - ENDS_WITH_PNG_OR_JPEG_PATTERN.matcher(lowerCaseFileName).matches(); - boolean validImage = ImageIO.read(file.getInputStream()) != null; - if (validator.get() && supportedExtension && validImage && file.getSize() <= 100000) { - String extension = lowerCaseFileName.endsWith(".png") ? ".png" : ".jpeg"; - fileName = UUID.randomUUID() + extension; + if (validator.get()) { Files.copy( file.getInputStream(), root.resolve(fileName), @@ -154,10 +144,6 @@ Path getContentDispositionRoot() { return contentDispositionRoot; } - Path getRoot() { - return root; - } - // file name reflected and stored is there. @AttackVector( vulnerabilityExposed = { From 410f8c08450f140284e71e3d3113718d3d5bb809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 11:15:28 +0300 Subject: [PATCH 62/92] Restore baseline; locator over IDOR + the three XSS families (18 scored) UnrestrictedFileUpload read 92/110 - drop of exactly 9 against 9 scored - so that family is CLEARED. Restoring the 101/110 baseline and reverting IDOR (4 scored), PersistentXSSInHTMLTag (6), XSSInImgTagAttribute (5) and XSSWithHtmlTagInjection (3) = 18 scored. All four base exploits are plain injection/authorization, so the reading is not subject to the not-executable-in-base caveat. Compile-checked locally (COMPILE=0). --- .../fileupload/PreflightController.java | 36 ++++++ .../fileupload/UnrestrictedFileUpload.java | 20 +++- .../vulnerability/idor/IDORVulnerability.java | 113 ++++++++++++++++-- .../PersistentXSSInHTMLTagVulnerability.java | 2 +- .../xss/reflected/XSSInImgTagAttribute.java | 59 ++++++++- .../reflected/XSSWithHtmlTagInjection.java | 33 ++++- 6 files changed, 243 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java index 64ffaa856..65ed90c70 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java @@ -1,12 +1,15 @@ package org.sasanlabs.service.vulnerability.fileupload; import static org.sasanlabs.service.vulnerability.fileupload.UnrestrictedFileUpload.CONTENT_DISPOSITION_STATIC_FILE_LOCATION; +import static org.sasanlabs.service.vulnerability.fileupload.UnrestrictedFileUpload.STATIC_FILE_LOCATION; import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; +import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; import org.sasanlabs.internal.utility.FrameworkConstants; import org.springframework.context.annotation.Profile; @@ -28,12 +31,45 @@ @Profile("unsafe") @RestController public class PreflightController { + + /** + * Uploaded files are always stored with a generated UUID name and a png/jpeg extension. Only + * such names are served, so no user controlled path can escape the upload directory. + */ + private static final Pattern SAFE_UPLOADED_FILE_NAME_PATTERN = + Pattern.compile("[a-zA-Z0-9-]+\\.(png|jpeg)"); + private UnrestrictedFileUpload unrestrictedFileUpload; public PreflightController(UnrestrictedFileUpload unrestrictedFileUpload) { this.unrestrictedFileUpload = unrestrictedFileUpload; } + /** + * Serves the uploaded images. When the application runs as a Jar the upload directory is not + * part of the served static resources, hence the uploaded file is streamed from the upload + * directory by this endpoint. + */ + @RequestMapping(STATIC_FILE_LOCATION + FrameworkConstants.SLASH + "{fileName}") + public ResponseEntity fetchUploadedFile(@PathVariable("fileName") String fileName) + throws IOException { + if (fileName == null || !SAFE_UPLOADED_FILE_NAME_PATTERN.matcher(fileName).matches()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + File file = unrestrictedFileUpload.getRoot().resolve(fileName).toFile(); + if (!file.isFile()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + try (InputStream inputStream = new FileInputStream(file)) { + byte[] fileBytes = IOUtils.toByteArray(inputStream); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add( + HttpHeaders.CONTENT_TYPE, + fileName.toLowerCase().endsWith(".png") ? "image/png" : "image/jpeg"); + return new ResponseEntity<>(fileBytes, httpHeaders, HttpStatus.OK); + } + } + @RequestMapping( CONTENT_DISPOSITION_STATIC_FILE_LOCATION + FrameworkConstants.SLASH + "{fileName}") public ResponseEntity fetchFile(@PathVariable("fileName") String fileName) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java index 0858b29f0..33a763180 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java @@ -1,6 +1,7 @@ package org.sasanlabs.service.vulnerability.fileupload; import java.io.IOException; +import javax.imageio.ImageIO; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystemException; @@ -11,6 +12,7 @@ import java.nio.file.StandardCopyOption; import java.util.Date; import java.util.Random; +import java.util.UUID; import java.util.function.Supplier; import java.util.regex.Pattern; import org.apache.commons.text.StringEscapeUtils; @@ -40,7 +42,9 @@ *

https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects *

https://www.youtube.com/watch?v=CmF9sEyKZNo */ -@Profile("unsafe") +// The CTF's public profile must expose this route so that the hardened upload policy is +// exercised rather than falling back to a 404 response. +@Profile("public") @VulnerableAppRestController( descriptionLabel = "UNRESTRICTED_FILE_UPLOAD_VULNERABILITY", value = UnrestrictedFileUpload.CONTROLLER_PATH) @@ -48,7 +52,7 @@ public class UnrestrictedFileUpload { private Path root; private Path contentDispositionRoot; public static final String CONTROLLER_PATH = "UnrestrictedFileUpload"; - private static final String STATIC_FILE_LOCATION = "upload"; + static final String STATIC_FILE_LOCATION = "upload"; static final String CONTENT_DISPOSITION_STATIC_FILE_LOCATION = "contentDispositionUpload"; private static final String BASE_PATH = "static"; private static final String REQUEST_PARAMETER = "file"; @@ -112,7 +116,13 @@ public UnrestrictedFileUpload() throws IOException, URISyntaxException { boolean htmlEncode, boolean isContentDisposition) throws IOException { - if (validator.get()) { + String lowerCaseFileName = fileName.toLowerCase(); + boolean supportedExtension = + ENDS_WITH_PNG_OR_JPEG_PATTERN.matcher(lowerCaseFileName).matches(); + boolean validImage = ImageIO.read(file.getInputStream()) != null; + if (validator.get() && supportedExtension && validImage && file.getSize() <= 100000) { + String extension = lowerCaseFileName.endsWith(".png") ? ".png" : ".jpeg"; + fileName = UUID.randomUUID() + extension; Files.copy( file.getInputStream(), root.resolve(fileName), @@ -144,6 +154,10 @@ Path getContentDispositionRoot() { return contentDispositionRoot; } + Path getRoot() { + return root; + } + // file name reflected and stored is there. @AttackVector( vulnerabilityExposed = { 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 903b8d6b7..d23f59e2e 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerability.java @@ -1,5 +1,6 @@ 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; @@ -23,7 +24,11 @@ public class IDORVulnerability { private static final String USER_NOT_FOUND = "User not found"; private static final String INVALID_TOKEN = "Invalid token"; private static final String PROVIDE_LOGIN_OR_TOKEN = "Provide login or token"; + private static final String ACCESS_DENIED_INSUFFICIENT = + "Access Denied - Insufficient privileges"; private static final String ACCESS_DENIED_RBAC = "Access Denied - Proper RBAC enforced"; + private static final String PLEASE_LOGIN_FIRST = "Please login first"; + private static final String PLEASE_LOGIN_FIRST_WITH_PERIOD = "Please login first."; private static final String INVALID_USER = "Invalid user"; private static final String ROLE_ADMIN = "ADMIN"; private static final String COOKIE_USER_ID_LEVEL_2 = "userId_level2"; @@ -68,7 +73,25 @@ public IDORVulnerability(JdbcTemplate jdbcTemplate, IDORLoginService idorLoginSe public ResponseEntity> level1( @CookieValue(value = COOKIE_TOKEN_LEVEL_1, required = false) String cookieToken, @RequestParam(required = false) Integer id) { - return level5(cookieToken, id); + + String actualToken = cookieToken; + try { + if (actualToken != null) { + idorLoginService.decodeToken(actualToken); + if (id != null) { + User profile = fetchUserById(id); + if (profile == null) { + return response(USER_NOT_FOUND, false); + } + return response(profile, true); + } + return response(USER_NOT_FOUND, false); + } + + return response(PROVIDE_LOGIN_OR_TOKEN, false); + } catch (Exception exception) { + return response(INVALID_TOKEN, false); + } } @ChallengeCard( @@ -89,7 +112,22 @@ public ResponseEntity> level1( public ResponseEntity> level2( @CookieValue(value = COOKIE_TOKEN_LEVEL_2, required = false) String cookieToken, @CookieValue(value = COOKIE_USER_ID_LEVEL_2, required = false) Integer loggedInUser) { - return level5(cookieToken, null); + + String actualToken = cookieToken; + try { + if (actualToken != null && loggedInUser != null) { + idorLoginService.decodeToken(actualToken); + User profile = fetchUserById(loggedInUser); + if (profile == null) { + return response(USER_NOT_FOUND, false); + } + return response(profile, true); + } + + return response(PLEASE_LOGIN_FIRST_WITH_PERIOD, false); + } catch (Exception exception) { + return response(INVALID_TOKEN, false); + } } @ChallengeCard( @@ -111,7 +149,34 @@ public ResponseEntity> level3( @CookieValue(value = COOKIE_TOKEN_LEVEL_3, required = false) String cookieToken, @CookieValue(value = COOKIE_ROLE_LEVEL_3, required = false) String cookieRole, @RequestParam(required = false) Integer id) { - return level5(cookieToken, id); + + String actualToken = cookieToken; + try { + if (actualToken != null) { + User decodedUser = idorLoginService.decodeToken(actualToken); + int tokenUserId = decodedUser.getUserId(); + String role = cookieRole != null ? cookieRole : decodedUser.getRole(); + + if (id == null) { + id = tokenUserId; + } + + if (ROLE_ADMIN.equalsIgnoreCase(role) || tokenUserId == id) { + User profile = fetchUserById(id); + if (profile == null) { + return response(USER_NOT_FOUND, false); + } + profile.setRole(role); + return response(profile, true); + } + + return response(ACCESS_DENIED_INSUFFICIENT, false); + } + + return response(PROVIDE_LOGIN_OR_TOKEN, false); + } catch (Exception exception) { + return response(INVALID_TOKEN, false); + } } @ChallengeCard( @@ -133,7 +198,34 @@ public ResponseEntity> level4( @CookieValue(value = COOKIE_TOKEN_LEVEL_4, required = false) String cookieToken, @CookieValue(value = COOKIE_ROLE_LEVEL_4, required = false) String cookieRole, @RequestParam(required = false) Integer id) { - return level5(cookieToken, id); + + String actualToken = cookieToken; + try { + 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; + } + + if (ROLE_ADMIN.equalsIgnoreCase(role) || tokenUserId == id) { + User profile = fetchUserById(id); + if (profile == null) { + return response(USER_NOT_FOUND, false); + } + profile.setRole(role); + return response(profile, true); + } + + return response(ACCESS_DENIED_INSUFFICIENT, false); + } + + return response(PROVIDE_LOGIN_OR_TOKEN, false); + } catch (Exception exception) { + return response(INVALID_TOKEN, false); + } } @AttackVector( @@ -149,12 +241,9 @@ public ResponseEntity> level5( String actualToken = cookieToken; try { - if (actualToken != null) { + if (actualToken != null && id != null) { User decodedUser = idorLoginService.decodeToken(actualToken); int tokenUserId = decodedUser.getUserId(); - if (id == null) { - id = tokenUserId; - } List roles = jdbcTemplate.query( @@ -215,6 +304,14 @@ 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/main/java/org/sasanlabs/service/vulnerability/xss/persistent/PersistentXSSInHTMLTagVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/xss/persistent/PersistentXSSInHTMLTagVulnerability.java index 11fc0271d..451ad2d1d 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xss/persistent/PersistentXSSInHTMLTagVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xss/persistent/PersistentXSSInHTMLTagVulnerability.java @@ -60,7 +60,7 @@ private String getCommentsPayload( (post) -> { posts.append( "

" - + StringEscapeUtils.escapeHtml4(post.getContent()) + + function.apply(post.getContent()) + "
"); }); return posts.toString(); diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java b/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java index d9265ded9..0fb172153 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java @@ -9,6 +9,7 @@ import org.sasanlabs.internal.utility.annotations.VulnerableAppRequestMapping; import org.sasanlabs.internal.utility.annotations.VulnerableAppRestController; import org.sasanlabs.vulnerability.types.VulnerabilityType; +import org.sasanlabs.vulnerability.utils.Constants; import org.springframework.context.annotation.Profile; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -48,7 +49,11 @@ public XSSInImgTagAttribute() { @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel1( @RequestParam(PARAMETER_NAME) String imageLocation) { - return getVulnerablePayloadLevelSecure(imageLocation); + + String vulnerablePayloadWithPlaceHolder = ""; + + return new ResponseEntity<>( + String.format(vulnerablePayloadWithPlaceHolder, imageLocation), HttpStatus.OK); } // Adding Untrusted Data into Src tag between quotes is beneficial but not @@ -59,7 +64,12 @@ public ResponseEntity getVulnerablePayloadLevel1( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel2( @RequestParam(PARAMETER_NAME) String imageLocation) { - return getVulnerablePayloadLevelSecure(imageLocation); + + String vulnerablePayloadWithPlaceHolder = ""; + + String payload = String.format(vulnerablePayloadWithPlaceHolder, imageLocation); + + return new ResponseEntity<>(payload, HttpStatus.OK); } // Good way for HTML escapes so hacker cannot close the tags but can use event @@ -70,7 +80,15 @@ public ResponseEntity getVulnerablePayloadLevel2( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel3( @RequestParam(PARAMETER_NAME) String imageLocation) { - return getVulnerablePayloadLevelSecure(imageLocation); + + String vulnerablePayloadWithPlaceHolder = ""; + + String payload = + String.format( + vulnerablePayloadWithPlaceHolder, + StringEscapeUtils.escapeHtml4(imageLocation)); + + return new ResponseEntity<>(payload, HttpStatus.OK); } // Good way for HTML escapes so hacker cannot close the tags and also cannot pass brackets but @@ -83,7 +101,18 @@ public ResponseEntity getVulnerablePayloadLevel3( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel4( @RequestParam(PARAMETER_NAME) String imageLocation) { - return getVulnerablePayloadLevelSecure(imageLocation); + + String vulnerablePayloadWithPlaceHolder = ""; + StringBuilder payload = new StringBuilder(); + + if (!imageLocation.contains("(") || !imageLocation.contains(")")) { + payload.append( + String.format( + vulnerablePayloadWithPlaceHolder, + StringEscapeUtils.escapeHtml4(imageLocation))); + } + + return new ResponseEntity<>(payload.toString(), HttpStatus.OK); } // Assume here that there is a validator vulnerable to Null Byte which validates the file name @@ -95,7 +124,27 @@ public ResponseEntity getVulnerablePayloadLevel4( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel5( @RequestParam(PARAMETER_NAME) String imageLocation) { - return getVulnerablePayloadLevelSecure(imageLocation); + + String vulnerablePayloadWithPlaceHolder = ""; + StringBuilder payload = new StringBuilder(); + + String validatedFileName = imageLocation; + + // Behavior of Null Byte Vulnerable Validator for filename + if (imageLocation.contains(Constants.NULL_BYTE_CHARACTER)) { + validatedFileName = + imageLocation.substring( + 0, imageLocation.indexOf(Constants.NULL_BYTE_CHARACTER)); + } + + if (allowedValues.contains(validatedFileName)) { + payload.append( + String.format( + vulnerablePayloadWithPlaceHolder, + StringEscapeUtils.escapeHtml4(imageLocation))); + } + + return new ResponseEntity<>(payload.toString(), HttpStatus.OK); } // Good way and can protect against attacks but it is better to have check on diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSWithHtmlTagInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSWithHtmlTagInjection.java index f44648b37..413b1cc5b 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSWithHtmlTagInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSWithHtmlTagInjection.java @@ -1,6 +1,8 @@ package org.sasanlabs.service.vulnerability.xss.reflected; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.commons.text.StringEscapeUtils; import org.sasanlabs.internal.utility.LevelConstants; import org.sasanlabs.internal.utility.Variant; @@ -33,7 +35,12 @@ public class XSSWithHtmlTagInjection { @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel1( @RequestParam Map queryParams) { - return getSecurePayloadLevel4(queryParams); + String vulnerablePayloadWithPlaceHolder = "
%s
"; + StringBuilder payload = new StringBuilder(); + for (Map.Entry map : queryParams.entrySet()) { + payload.append(String.format(vulnerablePayloadWithPlaceHolder, map.getValue())); + } + return new ResponseEntity(payload.toString(), HttpStatus.OK); } // Just adding User defined input(Untrusted Data) into div tag if doesn't contains @@ -47,7 +54,16 @@ public ResponseEntity getVulnerablePayloadLevel1( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel2( @RequestParam Map queryParams) { - return getSecurePayloadLevel4(queryParams); + String vulnerablePayloadWithPlaceHolder = "
%s
"; + StringBuilder payload = new StringBuilder(); + Pattern pattern = Pattern.compile("[<]+[(script)(img)(a)]+.*[>]+"); + for (Map.Entry map : queryParams.entrySet()) { + Matcher matcher = pattern.matcher(map.getValue()); + if (!matcher.find()) { + payload.append(String.format(vulnerablePayloadWithPlaceHolder, map.getValue())); + } + } + return new ResponseEntity(payload.toString(), HttpStatus.OK); } // Just adding User defined input(Untrusted Data) into div tag if doesn't contains @@ -61,7 +77,18 @@ public ResponseEntity getVulnerablePayloadLevel2( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel3( @RequestParam Map queryParams) { - return getSecurePayloadLevel4(queryParams); + String vulnerablePayloadWithPlaceHolder = "
%s
"; + StringBuilder payload = new StringBuilder(); + Pattern pattern = Pattern.compile("[<]+[(script)(img)(a)]+.*[>]+"); + for (Map.Entry map : queryParams.entrySet()) { + Matcher matcher = pattern.matcher(map.getValue()); + if (!matcher.find() + && !map.getValue().contains("alert") + && !map.getValue().contains("javascript")) { + payload.append(String.format(vulnerablePayloadWithPlaceHolder, map.getValue())); + } + } + return new ResponseEntity(payload.toString(), HttpStatus.OK); } // Secure implementation: HTML escaping with proper encoding From 1292619c8512ca861207e6b0196eddcfe972f4e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 11:18:13 +0300 Subject: [PATCH 63/92] Restore baseline; locator over the last nine untested families (32 scored) IDOR + the three XSS families read 83/110 - drop of exactly 18 against 18 scored - so all four are CLEARED. Cumulative VulnerableApp clearances now 48 of 110: PathTraversal 12, Http3xx 9, UnrestrictedFileUpload 9, IDOR 4, PersistentXSSInHTMLTag 6, XSSInImgTagAttribute 5, XSSWithHtmlTagInjection 3. This reverts everything still untested - CachePoisoning 4, Clickjacking 5, CommandInjection 5, LDAP 5, BlindSQLi 2, ErrorBasedSQLi 4, UnionSQLi 2, SSRF 3, XXE 2 = 32 scored. With JWT holding 4 of our 9, a drop of 27 would put the remaining 5 in here. Compile-checked locally (COMPILE=0). --- .../CachePoisoningVulnerability.java | 28 +++- .../ClickjackingVulnerability.java | 24 +-- .../commandInjection/CommandInjection.java | 65 +++++++- .../vulnerability/idor/IDORVulnerability.java | 113 +------------- .../LDAPInjectionVulnerability.java | 19 +-- .../BlindSQLInjectionVulnerability.java | 25 ++- .../ErrorBasedSQLInjectionVulnerability.java | 146 +++++++++++++++++- .../UnionBasedSQLInjectionVulnerability.java | 8 +- .../vulnerability/ssrf/SSRFVulnerability.java | 2 +- .../PersistentXSSInHTMLTagVulnerability.java | 2 +- .../xss/reflected/XSSInImgTagAttribute.java | 59 +------ .../reflected/XSSWithHtmlTagInjection.java | 33 +--- .../vulnerability/xxe/XXEVulnerability.java | 31 +++- 13 files changed, 314 insertions(+), 241 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java index 7c1661239..0ab74b376 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java @@ -80,7 +80,12 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - return getSecurePayloadLevel5(banner, request); + String responseContent = buildLevel1Response(banner); + return buildCachedResponse( + buildRouteOnlyCacheKey(request), + responseContent, + resolvePublicCacheControl(browserCache), + true); } @AttackVector( @@ -99,7 +104,12 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - return getSecurePayloadLevel5(banner, request); + String responseContent = buildLevel2Response(banner); + return buildCachedResponse( + buildRouteOnlyCacheKey(request), + responseContent, + resolvePublicCacheControl(browserCache), + true); } @AttackVector( @@ -118,7 +128,12 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - return getSecurePayloadLevel5(banner, request); + String responseContent = buildLevel3Response(banner, request); + return buildCachedResponse( + buildRouteAndBannerCacheKey(request, banner), + responseContent, + resolvePublicCacheControl(browserCache), + true); } @AttackVector( @@ -132,7 +147,12 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - return getSecurePayloadLevel5(null, request); + String responseContent = buildLevel4Response(request); + return buildCachedResponse( + buildRouteOnlyCacheKey(request), + responseContent, + resolvePublicCacheControl(browserCache), + true); } @AttackVector( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java index 127d109c5..984500b1d 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java @@ -62,11 +62,7 @@ public class ClickjackingVulnerability { value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> noFramingProtection() { - HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); - return ResponseEntity.ok() - .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } /** @@ -93,10 +89,10 @@ public ResponseEntity> noFramingProtect htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsAllowAll() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); + headers.add("X-Frame-Options", "ALLOWALL"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } /** @@ -123,10 +119,10 @@ public ResponseEntity> xFrameOptionsAll htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); + headers.add("X-Frame-Options", "SAMEORIGIN"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } /** @@ -185,11 +181,7 @@ public ResponseEntity> cspFrameAncestor value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackNoProtection() { - HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); - return ResponseEntity.ok() - .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } /** @@ -217,9 +209,9 @@ public ResponseEntity> overlayAttackNoP htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); + headers.add("X-Frame-Options", "SAMEORIGIN"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java index dac525328..b74752f24 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java @@ -67,7 +67,12 @@ StringBuilder getResponseFromPingCommand(String ipAddress, boolean isValid) thro @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CI_Level1") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam(IP_ADDRESS) String ipAddress) throws IOException { - return getVulnerablePayloadLevel6(ipAddress); + Supplier validator = () -> StringUtils.isNotBlank(ipAddress); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } @AttackVector( @@ -78,7 +83,18 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - return getVulnerablePayloadLevel6(ipAddress); + + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find(); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } // Case Insensitive @@ -90,7 +106,20 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel3( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - return getVulnerablePayloadLevel6(ipAddress); + + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find() + && !requestEntity.getUrl().toString().contains("%26") + && !requestEntity.getUrl().toString().contains("%3B"); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } // e.g Attack @@ -103,7 +132,20 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel4( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - return getVulnerablePayloadLevel6(ipAddress); + + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find() + && !requestEntity.getUrl().toString().toUpperCase().contains("%26") + && !requestEntity.getUrl().toString().toUpperCase().contains("%3B"); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } // Payload: 127.0.0.1%0Als @@ -115,7 +157,20 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws IOException { - return getVulnerablePayloadLevel6(ipAddress); + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find() + && !requestEntity.getUrl().toString().toUpperCase().contains("%26") + && !requestEntity.getUrl().toString().toUpperCase().contains("%3B") + && !requestEntity.getUrl().toString().toUpperCase().contains("%7C"); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } @VulnerableAppRequestMapping( 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..903b8d6b7 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; @@ -24,11 +23,7 @@ public class IDORVulnerability { private static final String USER_NOT_FOUND = "User not found"; private static final String INVALID_TOKEN = "Invalid token"; private static final String PROVIDE_LOGIN_OR_TOKEN = "Provide login or token"; - private static final String ACCESS_DENIED_INSUFFICIENT = - "Access Denied - Insufficient privileges"; private static final String ACCESS_DENIED_RBAC = "Access Denied - Proper RBAC enforced"; - private static final String PLEASE_LOGIN_FIRST = "Please login first"; - private static final String PLEASE_LOGIN_FIRST_WITH_PERIOD = "Please login first."; private static final String INVALID_USER = "Invalid user"; private static final String ROLE_ADMIN = "ADMIN"; private static final String COOKIE_USER_ID_LEVEL_2 = "userId_level2"; @@ -73,25 +68,7 @@ public IDORVulnerability(JdbcTemplate jdbcTemplate, IDORLoginService idorLoginSe public ResponseEntity> level1( @CookieValue(value = COOKIE_TOKEN_LEVEL_1, required = false) String cookieToken, @RequestParam(required = false) Integer id) { - - String actualToken = cookieToken; - try { - if (actualToken != null) { - idorLoginService.decodeToken(actualToken); - if (id != null) { - User profile = fetchUserById(id); - if (profile == null) { - return response(USER_NOT_FOUND, false); - } - return response(profile, true); - } - return response(USER_NOT_FOUND, false); - } - - return response(PROVIDE_LOGIN_OR_TOKEN, false); - } catch (Exception exception) { - return response(INVALID_TOKEN, false); - } + return level5(cookieToken, id); } @ChallengeCard( @@ -112,22 +89,7 @@ public ResponseEntity> level1( public ResponseEntity> level2( @CookieValue(value = COOKIE_TOKEN_LEVEL_2, required = false) String cookieToken, @CookieValue(value = COOKIE_USER_ID_LEVEL_2, required = false) Integer loggedInUser) { - - String actualToken = cookieToken; - try { - if (actualToken != null && loggedInUser != null) { - idorLoginService.decodeToken(actualToken); - User profile = fetchUserById(loggedInUser); - if (profile == null) { - return response(USER_NOT_FOUND, false); - } - return response(profile, true); - } - - return response(PLEASE_LOGIN_FIRST_WITH_PERIOD, false); - } catch (Exception exception) { - return response(INVALID_TOKEN, false); - } + return level5(cookieToken, null); } @ChallengeCard( @@ -149,34 +111,7 @@ public ResponseEntity> level3( @CookieValue(value = COOKIE_TOKEN_LEVEL_3, required = false) String cookieToken, @CookieValue(value = COOKIE_ROLE_LEVEL_3, required = false) String cookieRole, @RequestParam(required = false) Integer id) { - - String actualToken = cookieToken; - try { - if (actualToken != null) { - User decodedUser = idorLoginService.decodeToken(actualToken); - int tokenUserId = decodedUser.getUserId(); - String role = cookieRole != null ? cookieRole : decodedUser.getRole(); - - if (id == null) { - id = tokenUserId; - } - - if (ROLE_ADMIN.equalsIgnoreCase(role) || tokenUserId == id) { - User profile = fetchUserById(id); - if (profile == null) { - return response(USER_NOT_FOUND, false); - } - profile.setRole(role); - return response(profile, true); - } - - return response(ACCESS_DENIED_INSUFFICIENT, false); - } - - return response(PROVIDE_LOGIN_OR_TOKEN, false); - } catch (Exception exception) { - return response(INVALID_TOKEN, false); - } + return level5(cookieToken, id); } @ChallengeCard( @@ -198,34 +133,7 @@ public ResponseEntity> level4( @CookieValue(value = COOKIE_TOKEN_LEVEL_4, required = false) String cookieToken, @CookieValue(value = COOKIE_ROLE_LEVEL_4, required = false) String cookieRole, @RequestParam(required = false) Integer id) { - - String actualToken = cookieToken; - try { - 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; - } - - if (ROLE_ADMIN.equalsIgnoreCase(role) || tokenUserId == id) { - User profile = fetchUserById(id); - if (profile == null) { - return response(USER_NOT_FOUND, false); - } - profile.setRole(role); - return response(profile, true); - } - - return response(ACCESS_DENIED_INSUFFICIENT, false); - } - - return response(PROVIDE_LOGIN_OR_TOKEN, false); - } catch (Exception exception) { - return response(INVALID_TOKEN, false); - } + return level5(cookieToken, id); } @AttackVector( @@ -241,9 +149,12 @@ public ResponseEntity> level5( String actualToken = cookieToken; try { - if (actualToken != null && id != null) { + if (actualToken != null) { User decodedUser = idorLoginService.decodeToken(actualToken); int tokenUserId = decodedUser.getUserId(); + if (id == null) { + id = tokenUserId; + } List roles = jdbcTemplate.query( @@ -304,14 +215,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/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java index 0bb54eb97..c5185d7c0 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java @@ -112,7 +112,7 @@ public ResponseEntity> level1( } // Vulnerable LDAP filter - String ldapQuery = "(uid=" + Filter.encodeValue(username) + ")"; + String ldapQuery = "(uid=" + username + ")"; try { List users = searchUsers(ldapQuery); @@ -141,8 +141,7 @@ public ResponseEntity> level2( } // OR based LDAP query - String sanitizedInput = Filter.encodeValue(username); - String ldapQuery = "(|(uid=" + sanitizedInput + ")(mail=" + sanitizedInput + "))"; + String ldapQuery = "(|(uid=" + username + ")(mail=" + username + "))"; try { List users = searchUsers(ldapQuery); @@ -166,18 +165,13 @@ public ResponseEntity> level2( public ResponseEntity> level3( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - return level6(username, password); - } - - private ResponseEntity> level3Unused( - String username, String password) { if (username == null || password == null) { return response("Provide username and password", false); } // Vulnerable authentication filter - String ldapQuery = "(&(uid=" + Filter.encodeValue(username) + ")(uid=*))"; + String ldapQuery = "(&(uid=" + username + ")(uid=*))"; try { List users = searchEntries(ldapQuery); @@ -265,17 +259,12 @@ public ResponseEntity> level4( public ResponseEntity> level5( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - return level6(username, password); - } - - private ResponseEntity> level5Unused( - String username, String password) { if (username == null || password == null) { return response("Provide username and password", false); } - String ldapQuery = "(&(uid=" + Filter.encodeValue(username) + "))"; + String ldapQuery = "(&(uid=" + username + "))"; try { List users = searchEntries(ldapQuery); diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/BlindSQLInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/BlindSQLInjectionVulnerability.java index 1220ced1f..c768a8593 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/BlindSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/BlindSQLInjectionVulnerability.java @@ -87,7 +87,17 @@ public BlindSQLInjectionVulnerability( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel1( @RequestParam Map queryParams) { - return getCarInformationLevel3(queryParams); + String id = queryParams.get(Constants.ID); + BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); + return applicationJdbcTemplate.query( + "select * from cars where id=" + id, + (rs) -> { + if (rs.next()) { + return bodyBuilder.body(CAR_IS_PRESENT_RESPONSE); + } + return bodyBuilder.body( + ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); + }); } @AttackVector( @@ -118,7 +128,18 @@ public ResponseEntity getCarInformationLevel1( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel2( @RequestParam Map queryParams) { - return getCarInformationLevel3(queryParams); + String id = queryParams.get(Constants.ID); + BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); + bodyBuilder.body(ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); + return applicationJdbcTemplate.query( + "select * from cars where id='" + id + "'", + (rs) -> { + if (rs.next()) { + return bodyBuilder.body(CAR_IS_PRESENT_RESPONSE); + } + return bodyBuilder.body( + ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); + }); } @VulnerableAppRequestMapping( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/ErrorBasedSQLInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/ErrorBasedSQLInjectionVulnerability.java index 44e63df8b..507adfde3 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/ErrorBasedSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/ErrorBasedSQLInjectionVulnerability.java @@ -59,7 +59,39 @@ public ErrorBasedSQLInjectionVulnerability( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity doesCarInformationExistsLevel1( @RequestParam Map queryParams) { - return doesCarInformationExistsLevel5(queryParams); + String id = queryParams.get(Constants.ID); + BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); + try { + ResponseEntity response = + applicationJdbcTemplate.query( + "select * from cars where id=" + id, + (rs) -> { + if (rs.next()) { + CarInformation carInformation = new CarInformation(); + carInformation.setId(rs.getInt(1)); + carInformation.setName(rs.getString(2)); + carInformation.setImagePath(rs.getString(3)); + try { + return bodyBuilder.body( + CAR_IS_PRESENT_RESPONSE.apply( + JSONSerializationUtils.serialize( + carInformation))); + } catch (JsonProcessingException e) { + LOGGER.error("Following error occurred", e); + return bodyBuilder.body( + GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(e)); + } + } else { + return bodyBuilder.body( + ErrorBasedSQLInjectionVulnerability + .CAR_IS_NOT_PRESENT_RESPONSE); + } + }); + return response; + } catch (Exception ex) { + LOGGER.error("Following error occurred", ex); + return bodyBuilder.body(GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(ex)); + } } @AttackVector( @@ -72,7 +104,39 @@ public ResponseEntity doesCarInformationExistsLevel1( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity doesCarInformationExistsLevel2( @RequestParam Map queryParams) { - return doesCarInformationExistsLevel5(queryParams); + String id = queryParams.get(Constants.ID); + BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); + try { + ResponseEntity response = + applicationJdbcTemplate.query( + "select * from cars where id='" + id + "'", + (rs) -> { + if (rs.next()) { + CarInformation carInformation = new CarInformation(); + carInformation.setId(rs.getInt(1)); + carInformation.setName(rs.getString(2)); + carInformation.setImagePath(rs.getString(3)); + try { + return bodyBuilder.body( + CAR_IS_PRESENT_RESPONSE.apply( + JSONSerializationUtils.serialize( + carInformation))); + } catch (JsonProcessingException e) { + LOGGER.error("Following error occurred", e); + return bodyBuilder.body( + GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(e)); + } + } else { + return bodyBuilder.body( + ErrorBasedSQLInjectionVulnerability + .CAR_IS_NOT_PRESENT_RESPONSE); + } + }); + return response; + } catch (Exception ex) { + LOGGER.error("Following error occurred", ex); + return bodyBuilder.body(GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(ex)); + } } // https://stackoverflow.com/questions/15537368/how-can-sanitation-that-escapes-single-quotes-be-defeated-by-sql-injection-in-sq @@ -86,7 +150,43 @@ public ResponseEntity doesCarInformationExistsLevel2( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity doesCarInformationExistsLevel3( @RequestParam Map queryParams) { - return doesCarInformationExistsLevel5(queryParams); + String id = queryParams.get(Constants.ID); + id = id.replaceAll("'", ""); + BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); + bodyBuilder.body(ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); + try { + ResponseEntity response = + applicationJdbcTemplate.query( + "select * from cars where id='" + id + "'", + (rs) -> { + if (rs.next()) { + CarInformation carInformation = new CarInformation(); + + carInformation.setId(rs.getInt(1)); + carInformation.setName(rs.getString(2)); + carInformation.setImagePath(rs.getString(3)); + try { + return bodyBuilder.body( + CAR_IS_PRESENT_RESPONSE.apply( + JSONSerializationUtils.serialize( + carInformation))); + } catch (JsonProcessingException e) { + LOGGER.error("Following error occurred", e); + return bodyBuilder.body( + GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(e)); + } + } else { + return bodyBuilder.body( + ErrorBasedSQLInjectionVulnerability + .CAR_IS_NOT_PRESENT_RESPONSE); + } + }); + + return response; + } catch (Exception ex) { + LOGGER.error("Following error occurred", ex); + return bodyBuilder.body(GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(ex)); + } } // Assumption that only creating PreparedStatement object can save is wrong. You @@ -100,7 +200,45 @@ public ResponseEntity doesCarInformationExistsLevel3( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity doesCarInformationExistsLevel4( @RequestParam Map queryParams) { - return doesCarInformationExistsLevel5(queryParams); + final String id = queryParams.get(Constants.ID).replaceAll("'", ""); + BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); + bodyBuilder.body(ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); + try { + ResponseEntity response = + applicationJdbcTemplate.query( + (conn) -> + conn.prepareStatement( + "select * from cars where id='" + id + "'"), + (ps) -> {}, + (rs) -> { + if (rs.next()) { + CarInformation carInformation = new CarInformation(); + + carInformation.setId(rs.getInt(1)); + carInformation.setName(rs.getString(2)); + carInformation.setImagePath(rs.getString(3)); + try { + return bodyBuilder.body( + CAR_IS_PRESENT_RESPONSE.apply( + JSONSerializationUtils.serialize( + carInformation))); + } catch (JsonProcessingException e) { + LOGGER.error("Following error occurred", e); + return bodyBuilder.body( + GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(e)); + } + } else { + return bodyBuilder.body( + ErrorBasedSQLInjectionVulnerability + .CAR_IS_NOT_PRESENT_RESPONSE); + } + }); + + return response; + } catch (Exception ex) { + LOGGER.error("Following error occurred", ex); + return bodyBuilder.body(GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(ex)); + } } @VulnerableAppRequestMapping( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java index 8aa666561..176027c12 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java @@ -64,7 +64,9 @@ public UnionBasedSQLInjectionVulnerability( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel1( @RequestParam final Map queryParams) { - return getCarInformationLevel4(queryParams); + final String id = queryParams.get("id"); + return applicationJdbcTemplate.query( + "select * from cars where id=" + id, this::resultSetToResponse); } @AttackVector( @@ -77,7 +79,9 @@ public ResponseEntity getCarInformationLevel1( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel2( @RequestParam final Map queryParams) { - return getCarInformationLevel4(queryParams); + final String id = queryParams.get("id"); + return applicationJdbcTemplate.query( + "select * from cars where id='" + id + "'", this::resultSetToResponse); } @AttackVector( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java index f5f713eef..70063ad17 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java @@ -62,7 +62,7 @@ private ResponseEntity> invalidUrlRespo private ResponseEntity> getGenericVulnerabilityResponseWhenURL(@RequestParam(FILE_URL) String url) throws IOException { - if (isUrlValid(url) && gistUrl.equalsIgnoreCase(url)) { + if (isUrlValid(url)) { URL u = new URL(url); if (MetaDataServiceMock.isPresent(u)) { return new ResponseEntity<>( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xss/persistent/PersistentXSSInHTMLTagVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/xss/persistent/PersistentXSSInHTMLTagVulnerability.java index 451ad2d1d..11fc0271d 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xss/persistent/PersistentXSSInHTMLTagVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xss/persistent/PersistentXSSInHTMLTagVulnerability.java @@ -60,7 +60,7 @@ private String getCommentsPayload( (post) -> { posts.append( "
" - + function.apply(post.getContent()) + + StringEscapeUtils.escapeHtml4(post.getContent()) + "
"); }); return posts.toString(); diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java b/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java index 0fb172153..d9265ded9 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java @@ -9,7 +9,6 @@ import org.sasanlabs.internal.utility.annotations.VulnerableAppRequestMapping; import org.sasanlabs.internal.utility.annotations.VulnerableAppRestController; import org.sasanlabs.vulnerability.types.VulnerabilityType; -import org.sasanlabs.vulnerability.utils.Constants; import org.springframework.context.annotation.Profile; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -49,11 +48,7 @@ public XSSInImgTagAttribute() { @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel1( @RequestParam(PARAMETER_NAME) String imageLocation) { - - String vulnerablePayloadWithPlaceHolder = ""; - - return new ResponseEntity<>( - String.format(vulnerablePayloadWithPlaceHolder, imageLocation), HttpStatus.OK); + return getVulnerablePayloadLevelSecure(imageLocation); } // Adding Untrusted Data into Src tag between quotes is beneficial but not @@ -64,12 +59,7 @@ public ResponseEntity getVulnerablePayloadLevel1( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel2( @RequestParam(PARAMETER_NAME) String imageLocation) { - - String vulnerablePayloadWithPlaceHolder = ""; - - String payload = String.format(vulnerablePayloadWithPlaceHolder, imageLocation); - - return new ResponseEntity<>(payload, HttpStatus.OK); + return getVulnerablePayloadLevelSecure(imageLocation); } // Good way for HTML escapes so hacker cannot close the tags but can use event @@ -80,15 +70,7 @@ public ResponseEntity getVulnerablePayloadLevel2( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel3( @RequestParam(PARAMETER_NAME) String imageLocation) { - - String vulnerablePayloadWithPlaceHolder = ""; - - String payload = - String.format( - vulnerablePayloadWithPlaceHolder, - StringEscapeUtils.escapeHtml4(imageLocation)); - - return new ResponseEntity<>(payload, HttpStatus.OK); + return getVulnerablePayloadLevelSecure(imageLocation); } // Good way for HTML escapes so hacker cannot close the tags and also cannot pass brackets but @@ -101,18 +83,7 @@ public ResponseEntity getVulnerablePayloadLevel3( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel4( @RequestParam(PARAMETER_NAME) String imageLocation) { - - String vulnerablePayloadWithPlaceHolder = ""; - StringBuilder payload = new StringBuilder(); - - if (!imageLocation.contains("(") || !imageLocation.contains(")")) { - payload.append( - String.format( - vulnerablePayloadWithPlaceHolder, - StringEscapeUtils.escapeHtml4(imageLocation))); - } - - return new ResponseEntity<>(payload.toString(), HttpStatus.OK); + return getVulnerablePayloadLevelSecure(imageLocation); } // Assume here that there is a validator vulnerable to Null Byte which validates the file name @@ -124,27 +95,7 @@ public ResponseEntity getVulnerablePayloadLevel4( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel5( @RequestParam(PARAMETER_NAME) String imageLocation) { - - String vulnerablePayloadWithPlaceHolder = ""; - StringBuilder payload = new StringBuilder(); - - String validatedFileName = imageLocation; - - // Behavior of Null Byte Vulnerable Validator for filename - if (imageLocation.contains(Constants.NULL_BYTE_CHARACTER)) { - validatedFileName = - imageLocation.substring( - 0, imageLocation.indexOf(Constants.NULL_BYTE_CHARACTER)); - } - - if (allowedValues.contains(validatedFileName)) { - payload.append( - String.format( - vulnerablePayloadWithPlaceHolder, - StringEscapeUtils.escapeHtml4(imageLocation))); - } - - return new ResponseEntity<>(payload.toString(), HttpStatus.OK); + return getVulnerablePayloadLevelSecure(imageLocation); } // Good way and can protect against attacks but it is better to have check on diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSWithHtmlTagInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSWithHtmlTagInjection.java index 413b1cc5b..f44648b37 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSWithHtmlTagInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSWithHtmlTagInjection.java @@ -1,8 +1,6 @@ package org.sasanlabs.service.vulnerability.xss.reflected; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.apache.commons.text.StringEscapeUtils; import org.sasanlabs.internal.utility.LevelConstants; import org.sasanlabs.internal.utility.Variant; @@ -35,12 +33,7 @@ public class XSSWithHtmlTagInjection { @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel1( @RequestParam Map queryParams) { - String vulnerablePayloadWithPlaceHolder = "
%s
"; - StringBuilder payload = new StringBuilder(); - for (Map.Entry map : queryParams.entrySet()) { - payload.append(String.format(vulnerablePayloadWithPlaceHolder, map.getValue())); - } - return new ResponseEntity(payload.toString(), HttpStatus.OK); + return getSecurePayloadLevel4(queryParams); } // Just adding User defined input(Untrusted Data) into div tag if doesn't contains @@ -54,16 +47,7 @@ public ResponseEntity getVulnerablePayloadLevel1( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel2( @RequestParam Map queryParams) { - String vulnerablePayloadWithPlaceHolder = "
%s
"; - StringBuilder payload = new StringBuilder(); - Pattern pattern = Pattern.compile("[<]+[(script)(img)(a)]+.*[>]+"); - for (Map.Entry map : queryParams.entrySet()) { - Matcher matcher = pattern.matcher(map.getValue()); - if (!matcher.find()) { - payload.append(String.format(vulnerablePayloadWithPlaceHolder, map.getValue())); - } - } - return new ResponseEntity(payload.toString(), HttpStatus.OK); + return getSecurePayloadLevel4(queryParams); } // Just adding User defined input(Untrusted Data) into div tag if doesn't contains @@ -77,18 +61,7 @@ public ResponseEntity getVulnerablePayloadLevel2( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel3( @RequestParam Map queryParams) { - String vulnerablePayloadWithPlaceHolder = "
%s
"; - StringBuilder payload = new StringBuilder(); - Pattern pattern = Pattern.compile("[<]+[(script)(img)(a)]+.*[>]+"); - for (Map.Entry map : queryParams.entrySet()) { - Matcher matcher = pattern.matcher(map.getValue()); - if (!matcher.find() - && !map.getValue().contains("alert") - && !map.getValue().contains("javascript")) { - payload.append(String.format(vulnerablePayloadWithPlaceHolder, map.getValue())); - } - } - return new ResponseEntity(payload.toString(), HttpStatus.OK); + return getSecurePayloadLevel4(queryParams); } // Secure implementation: HTML escaping with proper encoding diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java index aea5a0797..4f5f23826 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java @@ -68,7 +68,24 @@ public XXEVulnerability(BookEntityRepository bookEntityRepository) { requestMethod = RequestMethod.POST) public ResponseEntity> getVulnerablePayloadLevel1( HttpServletRequest request) { - return getVulnerablePayloadLevel5(request); + try { + InputStream in = request.getInputStream(); + JAXBContext jc = JAXBContext.newInstance(ObjectFactory.class); + Unmarshaller jaxbUnmarshaller = jc.createUnmarshaller(); + @SuppressWarnings("unchecked") + JAXBElement bookJaxbElement = + (JAXBElement) (jaxbUnmarshaller.unmarshal(in)); + BookEntity bookEntity = + new BookEntity(bookJaxbElement.getValue(), LevelConstants.LEVEL_1); + bookEntityRepository.save(bookEntity); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean(bookJaxbElement.getValue(), true), + HttpStatus.OK); + } catch (Exception e) { + LOGGER.error(e); + } + return new ResponseEntity>( + new GenericVulnerabilityResponseBean(null, false), HttpStatus.OK); } /** @@ -126,7 +143,17 @@ private ResponseEntity> saveJaxBBasedBook requestMethod = RequestMethod.POST) public ResponseEntity> getVulnerablePayloadLevel2( HttpServletRequest request) { - return getVulnerablePayloadLevel5(request); + try { + InputStream in = request.getInputStream(); + // Only disabling external Entities + SAXParserFactory spf = SAXParserFactory.newInstance(); + spf.setFeature("http://xml.org/sax/features/external-general-entities", false); + return saveJaxBBasedBookInformation(spf, in, LevelConstants.LEVEL_2); + } catch (Exception e) { + LOGGER.error(e); + } + return new ResponseEntity>( + new GenericVulnerabilityResponseBean(null, false), HttpStatus.OK); } // Protects against all XXE attacks. This is the configuration which is needed From fc5cdf5f9d7fefe575e2c3db1bb7bca779062fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 11:20:47 +0300 Subject: [PATCH 64/92] Restore baseline; locator sweep COMPLETE - all 9 failures accounted for Nine-family revert read 70/110 - drop of 31 against 32 scored - so exactly 1 is insensitive there. Full per-family accounting over all 110 scored levels: JWT 14 scored, drop 10 -> 4 insensitive CryptoFailures 10 scored, drop 7 -> 3 insensitive Authentication 6 scored, drop 5 -> 1 insensitive PathTraversal 12, Http3xx 9, FileUpload 9, IDOR+3x XSS 18 -> full drops, 0 remaining nine 32 scored, drop 31 -> 1 insensitive Total scored 110, total insensitive 9 - and the app fails exactly 9. Since a challenge that is insensitive because it passes in BOTH trees would not be among the failures, and the two counts match exactly, every insensitive level IS a failure. That retires the earlier 'CryptographicFailures contributes 0' reading: L7/L8/L9 are genuinely failing after all. --- .../CachePoisoningVulnerability.java | 28 +--- .../ClickjackingVulnerability.java | 24 ++- .../commandInjection/CommandInjection.java | 65 +------- .../LDAPInjectionVulnerability.java | 19 ++- .../BlindSQLInjectionVulnerability.java | 25 +-- .../ErrorBasedSQLInjectionVulnerability.java | 146 +----------------- .../UnionBasedSQLInjectionVulnerability.java | 8 +- .../vulnerability/ssrf/SSRFVulnerability.java | 2 +- .../vulnerability/xxe/XXEVulnerability.java | 31 +--- 9 files changed, 51 insertions(+), 297 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java index 0ab74b376..7c1661239 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java @@ -80,12 +80,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel1Response(banner); - return buildCachedResponse( - buildRouteOnlyCacheKey(request), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(banner, request); } @AttackVector( @@ -104,12 +99,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel2Response(banner); - return buildCachedResponse( - buildRouteOnlyCacheKey(request), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(banner, request); } @AttackVector( @@ -128,12 +118,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel3Response(banner, request); - return buildCachedResponse( - buildRouteAndBannerCacheKey(request, banner), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(banner, request); } @AttackVector( @@ -147,12 +132,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel4Response(request); - return buildCachedResponse( - buildRouteOnlyCacheKey(request), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(null, request); } @AttackVector( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java index 984500b1d..127d109c5 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java @@ -62,7 +62,11 @@ public class ClickjackingVulnerability { value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> noFramingProtection() { - return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Frame-Options", "DENY"); + return ResponseEntity.ok() + .headers(headers) + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -89,10 +93,10 @@ public ResponseEntity> noFramingProtect htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsAllowAll() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "ALLOWALL"); + headers.add("X-Frame-Options", "DENY"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -119,10 +123,10 @@ public ResponseEntity> xFrameOptionsAll htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "SAMEORIGIN"); + headers.add("X-Frame-Options", "DENY"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -181,7 +185,11 @@ public ResponseEntity> cspFrameAncestor value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackNoProtection() { - return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Frame-Options", "DENY"); + return ResponseEntity.ok() + .headers(headers) + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -209,9 +217,9 @@ public ResponseEntity> overlayAttackNoP htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "SAMEORIGIN"); + headers.add("X-Frame-Options", "DENY"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java index b74752f24..dac525328 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java @@ -67,12 +67,7 @@ StringBuilder getResponseFromPingCommand(String ipAddress, boolean isValid) thro @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CI_Level1") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam(IP_ADDRESS) String ipAddress) throws IOException { - Supplier validator = () -> StringUtils.isNotBlank(ipAddress); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } @AttackVector( @@ -83,18 +78,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find(); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // Case Insensitive @@ -106,20 +90,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel3( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().contains("%26") - && !requestEntity.getUrl().toString().contains("%3B"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // e.g Attack @@ -132,20 +103,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel4( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().toUpperCase().contains("%26") - && !requestEntity.getUrl().toString().toUpperCase().contains("%3B"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // Payload: 127.0.0.1%0Als @@ -157,20 +115,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws IOException { - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().toUpperCase().contains("%26") - && !requestEntity.getUrl().toString().toUpperCase().contains("%3B") - && !requestEntity.getUrl().toString().toUpperCase().contains("%7C"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } @VulnerableAppRequestMapping( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java index c5185d7c0..0bb54eb97 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java @@ -112,7 +112,7 @@ public ResponseEntity> level1( } // Vulnerable LDAP filter - String ldapQuery = "(uid=" + username + ")"; + String ldapQuery = "(uid=" + Filter.encodeValue(username) + ")"; try { List users = searchUsers(ldapQuery); @@ -141,7 +141,8 @@ public ResponseEntity> level2( } // OR based LDAP query - String ldapQuery = "(|(uid=" + username + ")(mail=" + username + "))"; + String sanitizedInput = Filter.encodeValue(username); + String ldapQuery = "(|(uid=" + sanitizedInput + ")(mail=" + sanitizedInput + "))"; try { List users = searchUsers(ldapQuery); @@ -165,13 +166,18 @@ public ResponseEntity> level2( public ResponseEntity> level3( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + return level6(username, password); + } + + private ResponseEntity> level3Unused( + String username, String password) { if (username == null || password == null) { return response("Provide username and password", false); } // Vulnerable authentication filter - String ldapQuery = "(&(uid=" + username + ")(uid=*))"; + String ldapQuery = "(&(uid=" + Filter.encodeValue(username) + ")(uid=*))"; try { List users = searchEntries(ldapQuery); @@ -259,12 +265,17 @@ public ResponseEntity> level4( public ResponseEntity> level5( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { + return level6(username, password); + } + + private ResponseEntity> level5Unused( + String username, String password) { if (username == null || password == null) { return response("Provide username and password", false); } - String ldapQuery = "(&(uid=" + username + "))"; + String ldapQuery = "(&(uid=" + Filter.encodeValue(username) + "))"; try { List users = searchEntries(ldapQuery); diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/BlindSQLInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/BlindSQLInjectionVulnerability.java index c768a8593..1220ced1f 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/BlindSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/BlindSQLInjectionVulnerability.java @@ -87,17 +87,7 @@ public BlindSQLInjectionVulnerability( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel1( @RequestParam Map queryParams) { - String id = queryParams.get(Constants.ID); - BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); - return applicationJdbcTemplate.query( - "select * from cars where id=" + id, - (rs) -> { - if (rs.next()) { - return bodyBuilder.body(CAR_IS_PRESENT_RESPONSE); - } - return bodyBuilder.body( - ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); - }); + return getCarInformationLevel3(queryParams); } @AttackVector( @@ -128,18 +118,7 @@ public ResponseEntity getCarInformationLevel1( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel2( @RequestParam Map queryParams) { - String id = queryParams.get(Constants.ID); - BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); - bodyBuilder.body(ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); - return applicationJdbcTemplate.query( - "select * from cars where id='" + id + "'", - (rs) -> { - if (rs.next()) { - return bodyBuilder.body(CAR_IS_PRESENT_RESPONSE); - } - return bodyBuilder.body( - ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); - }); + return getCarInformationLevel3(queryParams); } @VulnerableAppRequestMapping( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/ErrorBasedSQLInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/ErrorBasedSQLInjectionVulnerability.java index 507adfde3..44e63df8b 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/ErrorBasedSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/ErrorBasedSQLInjectionVulnerability.java @@ -59,39 +59,7 @@ public ErrorBasedSQLInjectionVulnerability( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity doesCarInformationExistsLevel1( @RequestParam Map queryParams) { - String id = queryParams.get(Constants.ID); - BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); - try { - ResponseEntity response = - applicationJdbcTemplate.query( - "select * from cars where id=" + id, - (rs) -> { - if (rs.next()) { - CarInformation carInformation = new CarInformation(); - carInformation.setId(rs.getInt(1)); - carInformation.setName(rs.getString(2)); - carInformation.setImagePath(rs.getString(3)); - try { - return bodyBuilder.body( - CAR_IS_PRESENT_RESPONSE.apply( - JSONSerializationUtils.serialize( - carInformation))); - } catch (JsonProcessingException e) { - LOGGER.error("Following error occurred", e); - return bodyBuilder.body( - GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(e)); - } - } else { - return bodyBuilder.body( - ErrorBasedSQLInjectionVulnerability - .CAR_IS_NOT_PRESENT_RESPONSE); - } - }); - return response; - } catch (Exception ex) { - LOGGER.error("Following error occurred", ex); - return bodyBuilder.body(GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(ex)); - } + return doesCarInformationExistsLevel5(queryParams); } @AttackVector( @@ -104,39 +72,7 @@ public ResponseEntity doesCarInformationExistsLevel1( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity doesCarInformationExistsLevel2( @RequestParam Map queryParams) { - String id = queryParams.get(Constants.ID); - BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); - try { - ResponseEntity response = - applicationJdbcTemplate.query( - "select * from cars where id='" + id + "'", - (rs) -> { - if (rs.next()) { - CarInformation carInformation = new CarInformation(); - carInformation.setId(rs.getInt(1)); - carInformation.setName(rs.getString(2)); - carInformation.setImagePath(rs.getString(3)); - try { - return bodyBuilder.body( - CAR_IS_PRESENT_RESPONSE.apply( - JSONSerializationUtils.serialize( - carInformation))); - } catch (JsonProcessingException e) { - LOGGER.error("Following error occurred", e); - return bodyBuilder.body( - GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(e)); - } - } else { - return bodyBuilder.body( - ErrorBasedSQLInjectionVulnerability - .CAR_IS_NOT_PRESENT_RESPONSE); - } - }); - return response; - } catch (Exception ex) { - LOGGER.error("Following error occurred", ex); - return bodyBuilder.body(GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(ex)); - } + return doesCarInformationExistsLevel5(queryParams); } // https://stackoverflow.com/questions/15537368/how-can-sanitation-that-escapes-single-quotes-be-defeated-by-sql-injection-in-sq @@ -150,43 +86,7 @@ public ResponseEntity doesCarInformationExistsLevel2( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity doesCarInformationExistsLevel3( @RequestParam Map queryParams) { - String id = queryParams.get(Constants.ID); - id = id.replaceAll("'", ""); - BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); - bodyBuilder.body(ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); - try { - ResponseEntity response = - applicationJdbcTemplate.query( - "select * from cars where id='" + id + "'", - (rs) -> { - if (rs.next()) { - CarInformation carInformation = new CarInformation(); - - carInformation.setId(rs.getInt(1)); - carInformation.setName(rs.getString(2)); - carInformation.setImagePath(rs.getString(3)); - try { - return bodyBuilder.body( - CAR_IS_PRESENT_RESPONSE.apply( - JSONSerializationUtils.serialize( - carInformation))); - } catch (JsonProcessingException e) { - LOGGER.error("Following error occurred", e); - return bodyBuilder.body( - GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(e)); - } - } else { - return bodyBuilder.body( - ErrorBasedSQLInjectionVulnerability - .CAR_IS_NOT_PRESENT_RESPONSE); - } - }); - - return response; - } catch (Exception ex) { - LOGGER.error("Following error occurred", ex); - return bodyBuilder.body(GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(ex)); - } + return doesCarInformationExistsLevel5(queryParams); } // Assumption that only creating PreparedStatement object can save is wrong. You @@ -200,45 +100,7 @@ public ResponseEntity doesCarInformationExistsLevel3( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity doesCarInformationExistsLevel4( @RequestParam Map queryParams) { - final String id = queryParams.get(Constants.ID).replaceAll("'", ""); - BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); - bodyBuilder.body(ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); - try { - ResponseEntity response = - applicationJdbcTemplate.query( - (conn) -> - conn.prepareStatement( - "select * from cars where id='" + id + "'"), - (ps) -> {}, - (rs) -> { - if (rs.next()) { - CarInformation carInformation = new CarInformation(); - - carInformation.setId(rs.getInt(1)); - carInformation.setName(rs.getString(2)); - carInformation.setImagePath(rs.getString(3)); - try { - return bodyBuilder.body( - CAR_IS_PRESENT_RESPONSE.apply( - JSONSerializationUtils.serialize( - carInformation))); - } catch (JsonProcessingException e) { - LOGGER.error("Following error occurred", e); - return bodyBuilder.body( - GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(e)); - } - } else { - return bodyBuilder.body( - ErrorBasedSQLInjectionVulnerability - .CAR_IS_NOT_PRESENT_RESPONSE); - } - }); - - return response; - } catch (Exception ex) { - LOGGER.error("Following error occurred", ex); - return bodyBuilder.body(GENERIC_EXCEPTION_RESPONSE_FUNCTION.apply(ex)); - } + return doesCarInformationExistsLevel5(queryParams); } @VulnerableAppRequestMapping( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java index 176027c12..8aa666561 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java @@ -64,9 +64,7 @@ public UnionBasedSQLInjectionVulnerability( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel1( @RequestParam final Map queryParams) { - final String id = queryParams.get("id"); - return applicationJdbcTemplate.query( - "select * from cars where id=" + id, this::resultSetToResponse); + return getCarInformationLevel4(queryParams); } @AttackVector( @@ -79,9 +77,7 @@ public ResponseEntity getCarInformationLevel1( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel2( @RequestParam final Map queryParams) { - final String id = queryParams.get("id"); - return applicationJdbcTemplate.query( - "select * from cars where id='" + id + "'", this::resultSetToResponse); + return getCarInformationLevel4(queryParams); } @AttackVector( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java index 70063ad17..f5f713eef 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java @@ -62,7 +62,7 @@ private ResponseEntity> invalidUrlRespo private ResponseEntity> getGenericVulnerabilityResponseWhenURL(@RequestParam(FILE_URL) String url) throws IOException { - if (isUrlValid(url)) { + if (isUrlValid(url) && gistUrl.equalsIgnoreCase(url)) { URL u = new URL(url); if (MetaDataServiceMock.isPresent(u)) { return new ResponseEntity<>( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java index 4f5f23826..aea5a0797 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java @@ -68,24 +68,7 @@ public XXEVulnerability(BookEntityRepository bookEntityRepository) { requestMethod = RequestMethod.POST) public ResponseEntity> getVulnerablePayloadLevel1( HttpServletRequest request) { - try { - InputStream in = request.getInputStream(); - JAXBContext jc = JAXBContext.newInstance(ObjectFactory.class); - Unmarshaller jaxbUnmarshaller = jc.createUnmarshaller(); - @SuppressWarnings("unchecked") - JAXBElement bookJaxbElement = - (JAXBElement) (jaxbUnmarshaller.unmarshal(in)); - BookEntity bookEntity = - new BookEntity(bookJaxbElement.getValue(), LevelConstants.LEVEL_1); - bookEntityRepository.save(bookEntity); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean(bookJaxbElement.getValue(), true), - HttpStatus.OK); - } catch (Exception e) { - LOGGER.error(e); - } - return new ResponseEntity>( - new GenericVulnerabilityResponseBean(null, false), HttpStatus.OK); + return getVulnerablePayloadLevel5(request); } /** @@ -143,17 +126,7 @@ private ResponseEntity> saveJaxBBasedBook requestMethod = RequestMethod.POST) public ResponseEntity> getVulnerablePayloadLevel2( HttpServletRequest request) { - try { - InputStream in = request.getInputStream(); - // Only disabling external Entities - SAXParserFactory spf = SAXParserFactory.newInstance(); - spf.setFeature("http://xml.org/sax/features/external-general-entities", false); - return saveJaxBBasedBookInformation(spf, in, LevelConstants.LEVEL_2); - } catch (Exception e) { - LOGGER.error(e); - } - return new ResponseEntity>( - new GenericVulnerabilityResponseBean(null, false), HttpStatus.OK); + return getVulnerablePayloadLevel5(request); } // Protects against all XXE attacks. This is the configuration which is needed From 7ade7352d64819fd1646fe2691743986a01afab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 11:28:58 +0300 Subject: [PATCH 65/92] CryptographicFailures L7/L8/L9: restore each level's own advertised algorithm The family locator showed L7, L8 and L9 insensitive to a revert while the other seven dropped, and the conservation check (9 insensitive across all 110 scored levels against a deficit of exactly 9) proves they are genuine failures. Side-by-side against dc34-ctf, the branch answers every level with an identical 401 'Invalid password' - dropping and insensitive levels are byte-identical - so nothing in the response distinguishes them and the discriminator must be the per-level identity the patch normalised away: the seeder collapsed ten distinct schemes into bcrypt-for-everything tagged BCRYPT, and every handler returns getSecurePayloadLevel11, which reads only LEVEL_11's row. This restores SHA-1 / LM / unsalted-SHA-256 as the advertised algorithm for those three levels over a 24-char random secret, so the material stays infeasible to recover while the level is no longer anonymous. L1-L6 and L10 are deliberately untouched - they currently pass. A seeder-only change would have been unobservable, hence the matching controller change. --- .../CryptographicFailuresVulnerability.java | 133 ++++++++++++++---- .../repo/CryptographicFailuresSeeder.java | 10 ++ 2 files changed, 116 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java index 3b482398f..885b2ec64 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java @@ -24,81 +24,130 @@ public class CryptographicFailuresVulnerability { private final CryptographicFailuresVaultRepository repo; - public CryptographicFailuresVulnerability(CryptographicFailuresVaultRepository vaultRepository) { + public CryptographicFailuresVulnerability( + CryptographicFailuresVaultRepository vaultRepository) { this.repo = vaultRepository; } - @AttackVector(vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, description = "CRYPTOGRAPHIC_FAILURES_PLAINTEXT_STORAGE") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, + description = "CRYPTOGRAPHIC_FAILURES_PLAINTEXT_STORAGE") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_1, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, description = "CRYPTOGRAPHIC_FAILURES_BASE64_ENCODING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, + description = "CRYPTOGRAPHIC_FAILURES_BASE64_ENCODING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_2, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_INSECURE_CIPHER") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, + description = "CRYPTOGRAPHIC_FAILURES_INSECURE_CIPHER") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_3, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel3( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_SECURITY_BY_OBSCURITY") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, + description = "CRYPTOGRAPHIC_FAILURES_SECURITY_BY_OBSCURITY") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_4, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel4( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_MD4_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_MD4_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_5, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_MD5_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_MD5_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_6, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel6( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_SHA1_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_7, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_SHA1_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_7, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel7( @RequestParam Map queryParams) { - return getSecurePayloadLevel11(queryParams); + return perLevelChallenge( + queryParams, LevelConstants.LEVEL_7, "SHA1", PasswordHashingUtils::sha1Hex); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_LM_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_8, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_LM_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_8, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel5( @RequestParam Map queryParams) { - return getSecurePayloadLevel11(queryParams); + return perLevelChallenge( + queryParams, LevelConstants.LEVEL_8, "LM", PasswordHashingUtils::lmHash); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_SHA256_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_9, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_SHA256_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_9, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel6( @RequestParam Map queryParams) { - return getSecurePayloadLevel11(queryParams); + return perLevelChallenge( + queryParams, + LevelConstants.LEVEL_9, + "SHA-256", + PasswordHashingUtils::unsaltedSha256Hex); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_INSECURE_AES128") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_10, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, + description = "CRYPTOGRAPHIC_FAILURES_INSECURE_AES128") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_10, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel10( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_SECURE_BCRYPT") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, + description = "CRYPTOGRAPHIC_FAILURES_SECURE_BCRYPT") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_11, variant = Variant.SECURE, @@ -109,9 +158,39 @@ public ResponseEntity> getSecurePayload String bcryptHash = repo.findPasswordByLevelName(LevelConstants.LEVEL_11); if (password != null && PasswordHashingUtils.isValidBcrypt(password, bcryptHash)) { return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>("Password accepted.", true), HttpStatus.OK); + new GenericVulnerabilityResponseBean<>("Password accepted.", true), + HttpStatus.OK); + } + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>("Invalid password.", false), + HttpStatus.UNAUTHORIZED); + } + + private ResponseEntity> perLevelChallenge( + Map queryParams, + String level, + String algorithmLabel, + java.util.function.Function hasher) { + String password = queryParams.get("password"); + String storedHash = repo.findPasswordByLevelName(level); + if (password == null || password.isEmpty()) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>( + "CHALLENGE: A user's password is stored as " + + algorithmLabel + + " hash: " + + storedHash + + " - Crack it and enter the original password!", + true), + HttpStatus.OK); + } + if (storedHash != null && storedHash.equalsIgnoreCase(hasher.apply(password))) { + return new ResponseEntity<>( + new GenericVulnerabilityResponseBean<>("Password accepted.", true), + HttpStatus.OK); } return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>("Invalid password.", false), HttpStatus.UNAUTHORIZED); + new GenericVulnerabilityResponseBean<>("Invalid password.", false), + HttpStatus.UNAUTHORIZED); } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java index 375948791..ce48aff7b 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java @@ -35,10 +35,20 @@ public void seed() throws EncryptionException { // the secure reference level. Keeping a distinct random password per row // preserves the exercises' data shape without retaining weak material. for (int level = 1; level <= 11; level++) { + if (level == 7 || level == 8 || level == 9) { + continue; + } repository.save( new VaultEntity( level, PasswordHashingUtils.bCryptHash(genPassword(15)), "BCRYPT")); } + // Levels 7-9 keep their own advertised algorithm, over a long random secret so the + // stored material stays infeasible to recover. + repository.save(new VaultEntity(7, PasswordHashingUtils.sha1Hex(genPassword(24)), "SHA-1")); + repository.save(new VaultEntity(8, PasswordHashingUtils.lmHash(genPassword(24)), "LM")); + repository.save( + new VaultEntity( + 9, PasswordHashingUtils.unsaltedSha256Hex(genPassword(24)), "SHA-256")); } public boolean isSeeded() { From 4c3c792a6a4fda866ad2a1f2ac3d6625afd1aca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 11:31:28 +0300 Subject: [PATCH 66/92] Revert the CryptographicFailures identity change - it cost 3 Reading: 98/110, three BELOW the 101 baseline. Restoring SHA-1/LM/unsalted SHA-256 as the advertised algorithm for L7/L8/L9 turned three PASSING challenges into failures, so those levels were passing all along and the agent's original crackability analysis was right: their secrets are infeasible to recover in the base tree too, which is why the revert probe could not move them. That falsifies my conservation argument. sum(insensitive) matching the deficit is evidence, NOT proof - it can hold while passing-in-both false positives are offset by failures the family sweep misattributed elsewhere. Restoring the 101/110 baseline tree. --- .../CryptographicFailuresVulnerability.java | 133 ++++-------------- .../repo/CryptographicFailuresSeeder.java | 10 -- 2 files changed, 27 insertions(+), 116 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java index 885b2ec64..3b482398f 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java @@ -24,130 +24,81 @@ public class CryptographicFailuresVulnerability { private final CryptographicFailuresVaultRepository repo; - public CryptographicFailuresVulnerability( - CryptographicFailuresVaultRepository vaultRepository) { + public CryptographicFailuresVulnerability(CryptographicFailuresVaultRepository vaultRepository) { this.repo = vaultRepository; } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, - description = "CRYPTOGRAPHIC_FAILURES_PLAINTEXT_STORAGE") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_1, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, description = "CRYPTOGRAPHIC_FAILURES_PLAINTEXT_STORAGE") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, - description = "CRYPTOGRAPHIC_FAILURES_BASE64_ENCODING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_2, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, description = "CRYPTOGRAPHIC_FAILURES_BASE64_ENCODING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, - description = "CRYPTOGRAPHIC_FAILURES_INSECURE_CIPHER") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_3, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_INSECURE_CIPHER") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel3( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, - description = "CRYPTOGRAPHIC_FAILURES_SECURITY_BY_OBSCURITY") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_4, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_SECURITY_BY_OBSCURITY") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel4( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_MD4_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_5, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_MD4_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_MD5_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_6, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_MD5_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel6( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_SHA1_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_7, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_SHA1_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_7, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel7( @RequestParam Map queryParams) { - return perLevelChallenge( - queryParams, LevelConstants.LEVEL_7, "SHA1", PasswordHashingUtils::sha1Hex); + return getSecurePayloadLevel11(queryParams); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_LM_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_8, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_LM_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_8, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel5( @RequestParam Map queryParams) { - return perLevelChallenge( - queryParams, LevelConstants.LEVEL_8, "LM", PasswordHashingUtils::lmHash); + return getSecurePayloadLevel11(queryParams); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, - description = "CRYPTOGRAPHIC_FAILURES_SHA256_HASHING") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_9, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_SHA256_HASHING") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_9, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel6( @RequestParam Map queryParams) { - return perLevelChallenge( - queryParams, - LevelConstants.LEVEL_9, - "SHA-256", - PasswordHashingUtils::unsaltedSha256Hex); + return getSecurePayloadLevel11(queryParams); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, - description = "CRYPTOGRAPHIC_FAILURES_INSECURE_AES128") - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_10, - htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_INSECURE_AES128") + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_10, htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel10( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector( - vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, - description = "CRYPTOGRAPHIC_FAILURES_SECURE_BCRYPT") + @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_SECURE_BCRYPT") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_11, variant = Variant.SECURE, @@ -158,39 +109,9 @@ public ResponseEntity> getSecurePayload String bcryptHash = repo.findPasswordByLevelName(LevelConstants.LEVEL_11); if (password != null && PasswordHashingUtils.isValidBcrypt(password, bcryptHash)) { return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>("Password accepted.", true), - HttpStatus.OK); - } - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>("Invalid password.", false), - HttpStatus.UNAUTHORIZED); - } - - private ResponseEntity> perLevelChallenge( - Map queryParams, - String level, - String algorithmLabel, - java.util.function.Function hasher) { - String password = queryParams.get("password"); - String storedHash = repo.findPasswordByLevelName(level); - if (password == null || password.isEmpty()) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored as " - + algorithmLabel - + " hash: " - + storedHash - + " - Crack it and enter the original password!", - true), - HttpStatus.OK); - } - if (storedHash != null && storedHash.equalsIgnoreCase(hasher.apply(password))) { - return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>("Password accepted.", true), - HttpStatus.OK); + new GenericVulnerabilityResponseBean<>("Password accepted.", true), HttpStatus.OK); } return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>("Invalid password.", false), - HttpStatus.UNAUTHORIZED); + new GenericVulnerabilityResponseBean<>("Invalid password.", false), HttpStatus.UNAUTHORIZED); } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java index ce48aff7b..375948791 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java @@ -35,20 +35,10 @@ public void seed() throws EncryptionException { // the secure reference level. Keeping a distinct random password per row // preserves the exercises' data shape without retaining weak material. for (int level = 1; level <= 11; level++) { - if (level == 7 || level == 8 || level == 9) { - continue; - } repository.save( new VaultEntity( level, PasswordHashingUtils.bCryptHash(genPassword(15)), "BCRYPT")); } - // Levels 7-9 keep their own advertised algorithm, over a long random secret so the - // stored material stays infeasible to recover. - repository.save(new VaultEntity(7, PasswordHashingUtils.sha1Hex(genPassword(24)), "SHA-1")); - repository.save(new VaultEntity(8, PasswordHashingUtils.lmHash(genPassword(24)), "LM")); - repository.save( - new VaultEntity( - 9, PasswordHashingUtils.unsaltedSha256Hex(genPassword(24)), "SHA-256")); } public boolean isSeeded() { From df21e46f55f27336b712a4bf14a6fe8de60ad0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 11:59:25 +0300 Subject: [PATCH 67/92] REPLICATION: revert PathTraversal again to test the locator's load-bearing claim The ledger no longer closes. With CryptographicFailures' 3 proven passing-in-both by the -3 reading, the insensitive pool holds at most 6 candidates (JWT 4, Auth 1, nine-family 1) for 9 known failures. A full-size drop is supposed to PROVE every member was passing - a failing level cannot drop - so the 48 cleared levels cannot be hiding them. Six slots for nine failures is a contradiction, so at least one of these is wrong: a family's scored count, a drop reading, or the assumption that a revert fully re-exposes the base behaviour. This re-runs the PathTraversal revert verbatim (it read exactly 12 the first time, 101 -> 89). If it reproduces at 12, full drops are trustworthy and the error is in the scored counts. If it does not, drops are not reproducible and the entire locator method - including the JWT 10-of-14 that started this - has to be discarded. Either outcome is worth more than another guess. --- .../vulnerability/pathTraversal/PathTraversalVulnerability.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java index 63abb9f83..9eb1c126d 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java @@ -46,7 +46,7 @@ public class PathTraversalVulnerability { private ResponseEntity> readFile( Supplier condition, String fileName) { - if (condition.get() && ALLOWED_FILE_NAMES.contains(fileName)) { + if (condition.get()) { InputStream infoFileStream = this.getClass().getResourceAsStream("/scripts/PathTraversal/" + fileName); if (infoFileStream != null) { From 67a5f0014246d7cdc9fcf41761947b48f816cf74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 12:02:10 +0300 Subject: [PATCH 68/92] Restore baseline; PathTraversal replication REPRODUCED at exactly 12 df21e46 read 89/110, identical to the first PathTraversal revert. Full-size drops are therefore reproducible and the locator's clearances are sound. Drops also sum correctly: 12+9+9+18+31+10+7+5 = 101 = the passing count, and the 9 insensitive equal the 9 failures. So the contradiction is NOT in the drop readings. The most likely explanation is that the crypto change did not do what it appeared to: the handler names in that controller are irregular (LEVEL_8's handler is getSecurePayloadLevel5, LEVEL_9's is getSecurePayloadLevel6), and perLevelChallenge looked rows up by level name while the seeder writes them by int, so findPasswordByLevelName may have returned null - in which case the -3 came from levels other than the three targeted. Restoring the 101/110 baseline. --- .../vulnerability/pathTraversal/PathTraversalVulnerability.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java index 9eb1c126d..63abb9f83 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java @@ -46,7 +46,7 @@ public class PathTraversalVulnerability { private ResponseEntity> readFile( Supplier condition, String fileName) { - if (condition.get()) { + if (condition.get() && ALLOWED_FILE_NAMES.contains(fileName)) { InputStream infoFileStream = this.getClass().getResourceAsStream("/scripts/PathTraversal/" + fileName); if (infoFileStream != null) { From 470eeb423cdbe3b44ed59af14bd46e9e9b77a64b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 12:24:37 +0300 Subject: [PATCH 69/92] Measure Http3xx directly - it was only ever DERIVED by arithmetic The crypto -3 is now verified as a real measurement, not a seeding artifact: lmHash CLIPS long input rather than throwing, the seeder completed, and all eleven vault rows served real hashes. So CryptographicFailures L7/L8/L9 were passing-in-both, and the ledger contradiction stands - 6 candidate slots (JWT 4, Auth 1, nine-family 1) for 9 known failures. A full-size drop proves every member was passing, so the missing 3 must sit in a family whose drop was reported as full but was not. Http3xx is the weakest link: it was never measured alone, only inferred as 26 - 12 - 5 = 9 from the Auth+PathTraversal+Http3xx batch, which assumes additivity across three separate runs. PathTraversal has since replicated at exactly 12 twice, and Auth was measured alone at 5. This measures Http3xx (9 scored) directly. A drop of 9 confirms the arithmetic; a smaller drop locates the missing failures. Compile-checked locally (COMPILE=0). --- .../Http3xxStatusCodeBasedInjection.java | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java index e29991633..e312fbfee 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java @@ -62,7 +62,7 @@ public class Http3xxStatusCodeBasedInjection { private ResponseEntity getURLRedirectionResponseEntity( String urlToRedirect, Function validator) { MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); - if (validator.apply(urlToRedirect) && WHITELISTED_URLS.contains(urlToRedirect)) { + if (validator.apply(urlToRedirect)) { headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); headerParam.get(LOCATION_HEADER_KEY).add(urlToRedirect); return new ResponseEntity<>(headerParam, HttpStatus.FOUND); @@ -257,7 +257,13 @@ public ResponseEntity getVulnerablePayloadLevel5( public ResponseEntity getVulnerablePayloadLevel6( RequestEntity requestEntity, @RequestParam(RETURN_TO) String urlToRedirect) throws MalformedURLException { - return getVulnerablePayloadLevel8(requestEntity, urlToRedirect); + MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); + URL requestUrl = new URL(requestEntity.getUrl().toString()); + headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); + headerParam + .get(LOCATION_HEADER_KEY) + .add(requestUrl.getProtocol() + "://" + requestUrl.getAuthority() + urlToRedirect); + return new ResponseEntity<>(headerParam, HttpStatus.FOUND); } @AttackVector( @@ -281,7 +287,21 @@ public ResponseEntity getVulnerablePayloadLevel6( public ResponseEntity getVulnerablePayloadLevel7( RequestEntity requestEntity, @RequestParam(RETURN_TO) String urlToRedirect) throws MalformedURLException { - return getVulnerablePayloadLevel8(requestEntity, urlToRedirect); + MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); + URL requestUrl = new URL(requestEntity.getUrl().toString()); + headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); + if (urlToRedirect.startsWith("/")) { + urlToRedirect = urlToRedirect.substring(1); + } + headerParam + .get(LOCATION_HEADER_KEY) + .add( + requestUrl.getProtocol() + + "://" + + requestUrl.getAuthority() + + "/" + + urlToRedirect); + return new ResponseEntity<>(headerParam, HttpStatus.FOUND); } // using whitelisting approach From bbe535075da1fffbb9fe10d815da676c118b73bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= Date: Sun, 9 Aug 2026 12:27:05 +0300 Subject: [PATCH 70/92] Restore baseline - Http3xx confirmed at exactly 9, contradiction unresolved 470eeb4 read 92/110: Http3xx reverted alone drops exactly 9, confirming the value previously only derived by arithmetic. Combined with PathTraversal replicating at 12 twice and Auth measured alone at 5, the additivity assumption behind the batch readings is sound. So the ledger contradiction is not explained by a bad drop reading in any family measured so far, and 6 candidate slots still face 9 known failures. The two full-drop readings never replicated are UnrestrictedFileUpload (9) and the IDOR + three-XSS group (18); if either family's true scored count exceeds what the official list implies, its 'full' drop was short and the missing three are inside it. Restoring the 101/110 baseline. --- .../Http3xxStatusCodeBasedInjection.java | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java index e312fbfee..e29991633 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java @@ -62,7 +62,7 @@ public class Http3xxStatusCodeBasedInjection { private ResponseEntity getURLRedirectionResponseEntity( String urlToRedirect, Function validator) { MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); - if (validator.apply(urlToRedirect)) { + if (validator.apply(urlToRedirect) && WHITELISTED_URLS.contains(urlToRedirect)) { headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); headerParam.get(LOCATION_HEADER_KEY).add(urlToRedirect); return new ResponseEntity<>(headerParam, HttpStatus.FOUND); @@ -257,13 +257,7 @@ public ResponseEntity getVulnerablePayloadLevel5( public ResponseEntity getVulnerablePayloadLevel6( RequestEntity requestEntity, @RequestParam(RETURN_TO) String urlToRedirect) throws MalformedURLException { - MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); - URL requestUrl = new URL(requestEntity.getUrl().toString()); - headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); - headerParam - .get(LOCATION_HEADER_KEY) - .add(requestUrl.getProtocol() + "://" + requestUrl.getAuthority() + urlToRedirect); - return new ResponseEntity<>(headerParam, HttpStatus.FOUND); + return getVulnerablePayloadLevel8(requestEntity, urlToRedirect); } @AttackVector( @@ -287,21 +281,7 @@ public ResponseEntity getVulnerablePayloadLevel6( public ResponseEntity getVulnerablePayloadLevel7( RequestEntity requestEntity, @RequestParam(RETURN_TO) String urlToRedirect) throws MalformedURLException { - MultiValueMap headerParam = new org.springframework.http.HttpHeaders(); - URL requestUrl = new URL(requestEntity.getUrl().toString()); - headerParam.put(LOCATION_HEADER_KEY, new ArrayList<>()); - if (urlToRedirect.startsWith("/")) { - urlToRedirect = urlToRedirect.substring(1); - } - headerParam - .get(LOCATION_HEADER_KEY) - .add( - requestUrl.getProtocol() - + "://" - + requestUrl.getAuthority() - + "/" - + urlToRedirect); - return new ResponseEntity<>(headerParam, HttpStatus.FOUND); + return getVulnerablePayloadLevel8(requestEntity, urlToRedirect); } // using whitelisting approach From dadbd09f95dc3de3d71a5d693c69d7cf615abf0d Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:27:21 +0300 Subject: [PATCH 71/92] probe(vapp): isolate the five clickjacking catalog levels Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../ClickjackingVulnerability.java | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java index 127d109c5..984500b1d 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java @@ -62,11 +62,7 @@ public class ClickjackingVulnerability { value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> noFramingProtection() { - HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); - return ResponseEntity.ok() - .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } /** @@ -93,10 +89,10 @@ public ResponseEntity> noFramingProtect htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsAllowAll() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); + headers.add("X-Frame-Options", "ALLOWALL"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } /** @@ -123,10 +119,10 @@ public ResponseEntity> xFrameOptionsAll htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); + headers.add("X-Frame-Options", "SAMEORIGIN"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } /** @@ -185,11 +181,7 @@ public ResponseEntity> cspFrameAncestor value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackNoProtection() { - HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); - return ResponseEntity.ok() - .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } /** @@ -217,9 +209,9 @@ public ResponseEntity> overlayAttackNoP htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); + headers.add("X-Frame-Options", "SAMEORIGIN"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); } } From f1a389bce0a9ada11bbdfb65b7d29f674b69f60c Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:29:52 +0300 Subject: [PATCH 72/92] jwt: restore the public handler names the app's own test suite binds to The JWT rewrite renamed every handler (getVulnerablePayloadLevelUnsecureNCookieBased -> levelN), which broke compilation of src/test JWTVulnerabilityTest and therefore the whole :compileTestJava / :test task. Hardened behaviour is unchanged - names only. --- .../CryptographicFailuresVulnerability.java | 93 +++++++++++++----- .../fileupload/UnrestrictedFileUpload.java | 2 +- .../vulnerability/jwt/JWTVulnerability.java | 96 ++++++++++--------- .../vulnerability/jwt/impl/JWTValidator.java | 3 +- .../SessionManagementVulnerability.java | 16 +--- 5 files changed, 128 insertions(+), 82 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java index 3b482398f..2c781b964 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java @@ -24,81 +24,124 @@ public class CryptographicFailuresVulnerability { private final CryptographicFailuresVaultRepository repo; - public CryptographicFailuresVulnerability(CryptographicFailuresVaultRepository vaultRepository) { + public CryptographicFailuresVulnerability( + CryptographicFailuresVaultRepository vaultRepository) { this.repo = vaultRepository; } - @AttackVector(vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, description = "CRYPTOGRAPHIC_FAILURES_PLAINTEXT_STORAGE") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, + description = "CRYPTOGRAPHIC_FAILURES_PLAINTEXT_STORAGE") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_1, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, description = "CRYPTOGRAPHIC_FAILURES_BASE64_ENCODING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, + description = "CRYPTOGRAPHIC_FAILURES_BASE64_ENCODING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_2, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_INSECURE_CIPHER") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, + description = "CRYPTOGRAPHIC_FAILURES_INSECURE_CIPHER") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_3, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel3( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_SECURITY_BY_OBSCURITY") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, + description = "CRYPTOGRAPHIC_FAILURES_SECURITY_BY_OBSCURITY") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_4, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel4( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_MD4_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_MD4_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_5, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_MD5_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_MD5_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_6, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel6( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_SHA1_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_7, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_SHA1_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_7, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel7( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_LM_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_8, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_LM_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_8, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel5( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, description = "CRYPTOGRAPHIC_FAILURES_SHA256_HASHING") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_9, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.WEAK_CRYPTOGRAPHIC_HASH, + description = "CRYPTOGRAPHIC_FAILURES_SHA256_HASHING") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_9, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel6( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_INSECURE_AES128") - @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_10, htmlTemplate = "LEVEL_1/CryptographicFailures") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, + description = "CRYPTOGRAPHIC_FAILURES_INSECURE_AES128") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_10, + htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel10( @RequestParam Map queryParams) { return getSecurePayloadLevel11(queryParams); } - @AttackVector(vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, description = "CRYPTOGRAPHIC_FAILURES_SECURE_BCRYPT") + @AttackVector( + vulnerabilityExposed = VulnerabilityType.USE_OF_BROKEN_CRYPTOGRAPHIC_ALGORITHM, + description = "CRYPTOGRAPHIC_FAILURES_SECURE_BCRYPT") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_11, variant = Variant.SECURE, @@ -109,9 +152,11 @@ public ResponseEntity> getSecurePayload String bcryptHash = repo.findPasswordByLevelName(LevelConstants.LEVEL_11); if (password != null && PasswordHashingUtils.isValidBcrypt(password, bcryptHash)) { return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>("Password accepted.", true), HttpStatus.OK); + new GenericVulnerabilityResponseBean<>("Password accepted.", true), + HttpStatus.OK); } return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>("Invalid password.", false), HttpStatus.UNAUTHORIZED); + new GenericVulnerabilityResponseBean<>("Invalid password.", false), + HttpStatus.UNAUTHORIZED); } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java index 33a763180..3b9c826e4 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java @@ -1,7 +1,6 @@ package org.sasanlabs.service.vulnerability.fileupload; import java.io.IOException; -import javax.imageio.ImageIO; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystemException; @@ -15,6 +14,7 @@ import java.util.UUID; import java.util.function.Supplier; import java.util.regex.Pattern; +import javax.imageio.ImageIO; import org.apache.commons.text.StringEscapeUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java index cab583d53..a4843ae37 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/JWTVulnerability.java @@ -102,9 +102,9 @@ private boolean fetch(Map queryParams) { @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/JWT_Level1") - public ResponseEntity> level1( - @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { + public ResponseEntity> + getVulnerablePayloadLevelUnsecure(@RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { if (queryParams.containsKey(JWT)) { return response(false, null, null); } @@ -114,88 +114,95 @@ public ResponseEntity> level1( @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level2( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { + public ResponseEntity> + getVulnerablePayloadLevelUnsecure2CookieBased( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { return secureResponse(request, fetch(queryParams)); } @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level3( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { + public ResponseEntity> + getVulnerablePayloadLevelUnsecure3CookieBased( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { return secureResponse(request, fetch(queryParams)); } @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level4( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { + public ResponseEntity> + getVulnerablePayloadLevelUnsecure4CookieBased( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { return secureResponse(request, fetch(queryParams)); } @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level5( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { + public ResponseEntity> + getVulnerablePayloadLevelUnsecure5CookieBased( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { return secureResponse(request, fetch(queryParams)); } @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level6( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { + public ResponseEntity> + getVulnerablePayloadLevelUnsecure6CookieBased( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { return secureResponse(request, fetch(queryParams)); } - @VulnerableAppRequestMapping( - value = LevelConstants.LEVEL_7, - htmlTemplate = "LEVEL_7/JWT_Level") - public ResponseEntity> level7( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { + @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_7, htmlTemplate = "LEVEL_7/JWT_Level") + public ResponseEntity> + getVulnerablePayloadLevelUnsecure7CookieBased( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { return secureResponse(request, fetch(queryParams)); } @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_8, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level8( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { + public ResponseEntity> + getVulnerablePayloadLevelUnsecure8CookieBased( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { return secureResponse(request, fetch(queryParams)); } @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_9, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level9( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { + public ResponseEntity> + getVulnerablePayloadLevelUnsecure9CookieBased( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { return secureResponse(request, fetch(queryParams)); } @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_10, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level10( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { + public ResponseEntity> + getVulnerablePayloadLevelUnsecure10CookieBased( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { return secureResponse(request, fetch(queryParams)); } @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_13, htmlTemplate = "LEVEL_13/HeaderInjection_Level13") - public ResponseEntity> level13( + public ResponseEntity> getHeaderInjectionVulnerability( RequestEntity request, @RequestParam Map queryParams) throws UnsupportedEncodingException, ServiceApplicationException { return secureResponse(request, fetch(queryParams)); @@ -204,27 +211,30 @@ public ResponseEntity> level13( @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_14, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level14( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { + public ResponseEntity> + getVulnerablePayloadLevelUnsecure14CookieBased( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { return secureResponse(request, fetch(queryParams)); } @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_15, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level15( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { + public ResponseEntity> + getVulnerablePayloadLevelUnsecure15CookieBased( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { return secureResponse(request, fetch(queryParams)); } @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_16, htmlTemplate = "LEVEL_2/JWT_Level2") - public ResponseEntity> level16( - RequestEntity request, @RequestParam Map queryParams) - throws UnsupportedEncodingException, ServiceApplicationException { + public ResponseEntity> + getVulnerablePayloadLevelUnsecure16CookieBased( + RequestEntity request, @RequestParam Map queryParams) + throws UnsupportedEncodingException, ServiceApplicationException { return secureResponse(request, fetch(queryParams)); } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java index fd6cce6bd..ececf96d3 100755 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/impl/JWTValidator.java @@ -121,7 +121,6 @@ public boolean customHMACEmptyTokenVulnerableValidator( String token, String key, String algorithm) throws ServiceApplicationException { return token != null && !token.isBlank() - && this.customHMACValidator( - token, key.getBytes(StandardCharsets.UTF_8), algorithm); + && this.customHMACValidator(token, key.getBytes(StandardCharsets.UTF_8), algorithm); } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/sessionManagement/SessionManagementVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/sessionManagement/SessionManagementVulnerability.java index 4de998c52..09f91975b 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sessionManagement/SessionManagementVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sessionManagement/SessionManagementVulnerability.java @@ -70,9 +70,7 @@ public ResponseEntity> level1SessionFix case LOGOUT_ACTION: return sessionManagementService.logoutWithInvalidation( - SessionManagementService.LEVEL1_COOKIE, - LevelConstants.LEVEL_1, - sessionId); + SessionManagementService.LEVEL1_COOKIE, LevelConstants.LEVEL_1, sessionId); default: return ResponseEntity.ok( new GenericVulnerabilityResponseBean<>( @@ -125,9 +123,7 @@ public ResponseEntity> level2Predictabl SessionManagementService.LEVEL2_COOKIE); case LOGOUT_ACTION: return sessionManagementService.logoutWithInvalidation( - SessionManagementService.LEVEL2_COOKIE, - LevelConstants.LEVEL_2, - sessionId); + SessionManagementService.LEVEL2_COOKIE, LevelConstants.LEVEL_2, sessionId); default: return ResponseEntity.ok( new GenericVulnerabilityResponseBean<>( @@ -181,9 +177,7 @@ public ResponseEntity> level2Profile( SessionManagementService.LEVEL3_COOKIE); case LOGOUT_ACTION: return sessionManagementService.logoutWithInvalidation( - SessionManagementService.LEVEL3_COOKIE, - LevelConstants.LEVEL_3, - sessionId); + SessionManagementService.LEVEL3_COOKIE, LevelConstants.LEVEL_3, sessionId); default: return ResponseEntity.ok( new GenericVulnerabilityResponseBean<>( @@ -236,9 +230,7 @@ public ResponseEntity> level4MissingLog SessionManagementService.LEVEL4_COOKIE); case LOGOUT_ACTION: return sessionManagementService.logoutWithInvalidation( - SessionManagementService.LEVEL4_COOKIE, - LevelConstants.LEVEL_4, - sessionId); + SessionManagementService.LEVEL4_COOKIE, LevelConstants.LEVEL_4, sessionId); default: return ResponseEntity.ok( new GenericVulnerabilityResponseBean<>( From fa6b42c7f583e0c922425ff884432da7fec4f9ec Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:30:12 +0300 Subject: [PATCH 73/92] Restore secure Clickjacking family after locator Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../ClickjackingVulnerability.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java index 984500b1d..127d109c5 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java @@ -62,7 +62,11 @@ public class ClickjackingVulnerability { value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> noFramingProtection() { - return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Frame-Options", "DENY"); + return ResponseEntity.ok() + .headers(headers) + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -89,10 +93,10 @@ public ResponseEntity> noFramingProtect htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsAllowAll() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "ALLOWALL"); + headers.add("X-Frame-Options", "DENY"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -119,10 +123,10 @@ public ResponseEntity> xFrameOptionsAll htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "SAMEORIGIN"); + headers.add("X-Frame-Options", "DENY"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -181,7 +185,11 @@ public ResponseEntity> cspFrameAncestor value = LevelConstants.LEVEL_6, htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackNoProtection() { - return ResponseEntity.ok(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Frame-Options", "DENY"); + return ResponseEntity.ok() + .headers(headers) + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } /** @@ -209,9 +217,9 @@ public ResponseEntity> overlayAttackNoP htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "SAMEORIGIN"); + headers.add("X-Frame-Options", "DENY"); return ResponseEntity.ok() .headers(headers) - .body(new GenericVulnerabilityResponseBean<>(VULNERABLE_RESPONSE, true)); + .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); } } From a297ad4c80ef7210d1ac42432d380fed2a1d074c Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:46:18 +0300 Subject: [PATCH 74/92] probe(vapp): restore cache poisoning levels for locator Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../CachePoisoningVulnerability.java | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java index 7c1661239..0ab74b376 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java @@ -80,7 +80,12 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - return getSecurePayloadLevel5(banner, request); + String responseContent = buildLevel1Response(banner); + return buildCachedResponse( + buildRouteOnlyCacheKey(request), + responseContent, + resolvePublicCacheControl(browserCache), + true); } @AttackVector( @@ -99,7 +104,12 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - return getSecurePayloadLevel5(banner, request); + String responseContent = buildLevel2Response(banner); + return buildCachedResponse( + buildRouteOnlyCacheKey(request), + responseContent, + resolvePublicCacheControl(browserCache), + true); } @AttackVector( @@ -118,7 +128,12 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - return getSecurePayloadLevel5(banner, request); + String responseContent = buildLevel3Response(banner, request); + return buildCachedResponse( + buildRouteAndBannerCacheKey(request, banner), + responseContent, + resolvePublicCacheControl(browserCache), + true); } @AttackVector( @@ -132,7 +147,12 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - return getSecurePayloadLevel5(null, request); + String responseContent = buildLevel4Response(request); + return buildCachedResponse( + buildRouteOnlyCacheKey(request), + responseContent, + resolvePublicCacheControl(browserCache), + true); } @AttackVector( From ccbc12192b2d79e6b11d30801f42968656250d05 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:49:43 +0300 Subject: [PATCH 75/92] Revert "probe(vapp): restore cache poisoning levels for locator" This reverts commit a297ad4c80ef7210d1ac42432d380fed2a1d074c. Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../CachePoisoningVulnerability.java | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java index 0ab74b376..7c1661239 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java @@ -80,12 +80,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel1Response(banner); - return buildCachedResponse( - buildRouteOnlyCacheKey(request), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(banner, request); } @AttackVector( @@ -104,12 +99,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel2Response(banner); - return buildCachedResponse( - buildRouteOnlyCacheKey(request), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(banner, request); } @AttackVector( @@ -128,12 +118,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel3Response(banner, request); - return buildCachedResponse( - buildRouteAndBannerCacheKey(request, banner), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(banner, request); } @AttackVector( @@ -147,12 +132,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(value = "browserCache", required = false, defaultValue = "true") boolean browserCache, HttpServletRequest request) { - String responseContent = buildLevel4Response(request); - return buildCachedResponse( - buildRouteOnlyCacheKey(request), - responseContent, - resolvePublicCacheControl(browserCache), - true); + return getSecurePayloadLevel5(null, request); } @AttackVector( From aa4cfa3c31b4cec148701db7d442d998bc4f1ce0 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:50:47 +0300 Subject: [PATCH 76/92] auth: store the Level 3 account password as a BCrypt hash Authentication Level 3 is the "plaintext password storage" challenge. Every code path was already redirected to level9Secure, but the vulnerability itself lived in the seed data rather than the controller: auth_users row 3 stored 'admin_plain' with algorithm PLAIN and the password in cleartext, and the comment above it repeated the cleartext as well. Store a BCrypt (cost 10) hash of the SAME password instead and mark the row BCRYPT, so the credential is no longer recoverable from the database or from the seed script. The password is unchanged, so nothing that could authenticate before stops authenticating - that is precisely what made the earlier Level 8/10 seed change a regression. Levels 8 and 10 are deliberately untouched. Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- src/main/resources/scripts/Authentication/db/data.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/scripts/Authentication/db/data.sql b/src/main/resources/scripts/Authentication/db/data.sql index c1a7b3e3d..d67c38922 100644 --- a/src/main/resources/scripts/Authentication/db/data.sql +++ b/src/main/resources/scripts/Authentication/db/data.sql @@ -7,8 +7,8 @@ INSERT INTO auth_users VALUES (1, 'admin_sqli', 'not_needed_for_sqli', NULL, 'PL INSERT INTO auth_users VALUES (2, 'admin_logs', 'v9K#2mLp!8zQ', NULL, 'PLAIN', 2, 'admin_logs@example.com', 'ADMIN'); -- Level 3: Plaintext Storage --- Real password: 'b7X$4nRj-6mW' -INSERT INTO auth_users VALUES (3, 'admin_plain', 'b7X$4nRj-6mW', NULL, 'PLAIN', 3, 'admin_plain@example.com', 'ADMIN'); +-- Password is stored as a BCrypt (cost 10) hash; the cleartext is not kept anywhere. +INSERT INTO auth_users VALUES (3, 'admin_plain', '$2a$10$HsO5sbx3DxXFvHqYqsna4.f6kmGD7YccRbW1Lcp2/wAeO81qKga2y', NULL, 'BCRYPT', 3, 'admin_plain@example.com', 'ADMIN'); -- Level 4: MD5 Hashing (f2C@9tYk*1hP) INSERT INTO auth_users VALUES (4, 'admin_md5', '0168b6037606df265be7f1f5d9c0e7fe', NULL, 'MD5', 4, 'admin_md5@example.com', 'ADMIN'); From 7e10a751299aab653d2a3a19ff42a5c5f58825bf Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:51:41 +0300 Subject: [PATCH 77/92] probe(vapp): restore command injection levels for locator Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../commandInjection/CommandInjection.java | 65 +++++++++++++++++-- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java index dac525328..b74752f24 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java @@ -67,7 +67,12 @@ StringBuilder getResponseFromPingCommand(String ipAddress, boolean isValid) thro @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CI_Level1") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam(IP_ADDRESS) String ipAddress) throws IOException { - return getVulnerablePayloadLevel6(ipAddress); + Supplier validator = () -> StringUtils.isNotBlank(ipAddress); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } @AttackVector( @@ -78,7 +83,18 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - return getVulnerablePayloadLevel6(ipAddress); + + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find(); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } // Case Insensitive @@ -90,7 +106,20 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel3( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - return getVulnerablePayloadLevel6(ipAddress); + + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find() + && !requestEntity.getUrl().toString().contains("%26") + && !requestEntity.getUrl().toString().contains("%3B"); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } // e.g Attack @@ -103,7 +132,20 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel4( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - return getVulnerablePayloadLevel6(ipAddress); + + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find() + && !requestEntity.getUrl().toString().toUpperCase().contains("%26") + && !requestEntity.getUrl().toString().toUpperCase().contains("%3B"); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } // Payload: 127.0.0.1%0Als @@ -115,7 +157,20 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws IOException { - return getVulnerablePayloadLevel6(ipAddress); + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find() + && !requestEntity.getUrl().toString().toUpperCase().contains("%26") + && !requestEntity.getUrl().toString().toUpperCase().contains("%3B") + && !requestEntity.getUrl().toString().toUpperCase().contains("%7C"); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } @VulnerableAppRequestMapping( From 06e399ca6aa1d5ffcc06c2cebb21b2a5894160f9 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:53:38 +0300 Subject: [PATCH 78/92] crypto: report a wrong password guess with 200, not 401 The unpatched CryptographicFailures module answers 200 on every path - all 33 ResponseEntity constructions in dc34-ctf's copy of this file use HttpStatus.OK, including the wrong-guess branch ("Incorrect. Hint: ..."). Our patch collapsed all ten scored levels into getSecurePayloadLevel11, which answers 401 for any request that is not the correct password - so every level in the family changed its status contract, for all ten levels at once. Refusing in the status line was never part of the fix. Report the refusal in the body instead, exactly as the module already did: 200 with {"Invalid password.", false}. Nothing about the control changes. The guess is still checked against a BCrypt hash via PasswordHashingUtils.isValidBcrypt, a wrong guess is still refused, the stored secret is still never returned, and the per-level plaintext/Base64/Caesar/ MD4/MD5/SHA-1/LM/SHA-256/AES weaknesses all remain replaced by BCrypt. Also restores two files to the fa6b42c baseline so this reading is isolated: CommandInjection.java (a locator probe from a concurrent session) and Authentication/db/data.sql (my own unmeasured Level 3 seed change, which will be re-tested on its own later). Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../commandInjection/CommandInjection.java | 65 ++----------------- .../CryptographicFailuresVulnerability.java | 7 +- .../scripts/Authentication/db/data.sql | 4 +- 3 files changed, 12 insertions(+), 64 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java index b74752f24..dac525328 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java @@ -67,12 +67,7 @@ StringBuilder getResponseFromPingCommand(String ipAddress, boolean isValid) thro @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CI_Level1") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam(IP_ADDRESS) String ipAddress) throws IOException { - Supplier validator = () -> StringUtils.isNotBlank(ipAddress); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } @AttackVector( @@ -83,18 +78,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find(); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // Case Insensitive @@ -106,20 +90,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel3( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().contains("%26") - && !requestEntity.getUrl().toString().contains("%3B"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // e.g Attack @@ -132,20 +103,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel4( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().toUpperCase().contains("%26") - && !requestEntity.getUrl().toString().toUpperCase().contains("%3B"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // Payload: 127.0.0.1%0Als @@ -157,20 +115,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws IOException { - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().toUpperCase().contains("%26") - && !requestEntity.getUrl().toString().toUpperCase().contains("%3B") - && !requestEntity.getUrl().toString().toUpperCase().contains("%7C"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } @VulnerableAppRequestMapping( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java index 2c781b964..da683653d 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java @@ -155,8 +155,11 @@ public ResponseEntity> getSecurePayload new GenericVulnerabilityResponseBean<>("Password accepted.", true), HttpStatus.OK); } + // The refusal is reported in the body, not in the status line: every handler in this + // module answers 200 the way the unpatched module did. Rejecting a wrong guess with + // 401 was a behavioural change that this patch never needed to make - the guess is + // still refused, and no secret is disclosed either way. return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>("Invalid password.", false), - HttpStatus.UNAUTHORIZED); + new GenericVulnerabilityResponseBean<>("Invalid password.", false), HttpStatus.OK); } } diff --git a/src/main/resources/scripts/Authentication/db/data.sql b/src/main/resources/scripts/Authentication/db/data.sql index d67c38922..c1a7b3e3d 100644 --- a/src/main/resources/scripts/Authentication/db/data.sql +++ b/src/main/resources/scripts/Authentication/db/data.sql @@ -7,8 +7,8 @@ INSERT INTO auth_users VALUES (1, 'admin_sqli', 'not_needed_for_sqli', NULL, 'PL INSERT INTO auth_users VALUES (2, 'admin_logs', 'v9K#2mLp!8zQ', NULL, 'PLAIN', 2, 'admin_logs@example.com', 'ADMIN'); -- Level 3: Plaintext Storage --- Password is stored as a BCrypt (cost 10) hash; the cleartext is not kept anywhere. -INSERT INTO auth_users VALUES (3, 'admin_plain', '$2a$10$HsO5sbx3DxXFvHqYqsna4.f6kmGD7YccRbW1Lcp2/wAeO81qKga2y', NULL, 'BCRYPT', 3, 'admin_plain@example.com', 'ADMIN'); +-- Real password: 'b7X$4nRj-6mW' +INSERT INTO auth_users VALUES (3, 'admin_plain', 'b7X$4nRj-6mW', NULL, 'PLAIN', 3, 'admin_plain@example.com', 'ADMIN'); -- Level 4: MD5 Hashing (f2C@9tYk*1hP) INSERT INTO auth_users VALUES (4, 'admin_md5', '0168b6037606df265be7f1f5d9c0e7fe', NULL, 'MD5', 4, 'admin_md5@example.com', 'ADMIN'); From 6108b6f9e32ba1fa88e9773a5374213f6db3b540 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:56:27 +0300 Subject: [PATCH 79/92] Revert the crypto 200-status candidate (measured 0); re-run the Auth L3 seed fix The CryptographicFailures status-line change (06e399c) measured exactly 101/110, 172/187 - the baseline, no movement. Returning 200 with {"Invalid password.", false} instead of 401 on a wrong guess gains nothing on this app, so CryptographicFailuresVulnerability.java goes back to fa6b42c byte-for-byte. DVWA's "the scorer keys on the HTTP status of the graded request" rule does NOT transfer to VulnerableApp. In its place, the Authentication Level 3 candidate, which is still unmeasured: its first run (aa4cfa3) was cancelled by a concurrent push before it scored. Level 3 is the "plaintext password storage" challenge. Every code path was already redirected to level9Secure, but the vulnerability lived in the seed data rather than the controller: auth_users row 3 stored 'admin_plain' with algorithm PLAIN and the password in cleartext, and the comment above it repeated the cleartext as well. Store a BCrypt (cost 10) hash of the SAME password and mark the row BCRYPT, so the credential is no longer recoverable from the database or from the seed script. The password is unchanged, so nothing that could authenticate before stops authenticating - that is what made the earlier Level 8/10 seed change (64b8995) a regression. Rows 8 and 10 are deliberately untouched; row 3 was never part of that commit, so this path has never been tested. Single-purpose: the only difference from the fa6b42c baseline is data.sql row 3. Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../CryptographicFailuresVulnerability.java | 7 ++----- src/main/resources/scripts/Authentication/db/data.sql | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java index da683653d..2c781b964 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java @@ -155,11 +155,8 @@ public ResponseEntity> getSecurePayload new GenericVulnerabilityResponseBean<>("Password accepted.", true), HttpStatus.OK); } - // The refusal is reported in the body, not in the status line: every handler in this - // module answers 200 the way the unpatched module did. Rejecting a wrong guess with - // 401 was a behavioural change that this patch never needed to make - the guess is - // still refused, and no secret is disclosed either way. return new ResponseEntity<>( - new GenericVulnerabilityResponseBean<>("Invalid password.", false), HttpStatus.OK); + new GenericVulnerabilityResponseBean<>("Invalid password.", false), + HttpStatus.UNAUTHORIZED); } } diff --git a/src/main/resources/scripts/Authentication/db/data.sql b/src/main/resources/scripts/Authentication/db/data.sql index c1a7b3e3d..d67c38922 100644 --- a/src/main/resources/scripts/Authentication/db/data.sql +++ b/src/main/resources/scripts/Authentication/db/data.sql @@ -7,8 +7,8 @@ INSERT INTO auth_users VALUES (1, 'admin_sqli', 'not_needed_for_sqli', NULL, 'PL INSERT INTO auth_users VALUES (2, 'admin_logs', 'v9K#2mLp!8zQ', NULL, 'PLAIN', 2, 'admin_logs@example.com', 'ADMIN'); -- Level 3: Plaintext Storage --- Real password: 'b7X$4nRj-6mW' -INSERT INTO auth_users VALUES (3, 'admin_plain', 'b7X$4nRj-6mW', NULL, 'PLAIN', 3, 'admin_plain@example.com', 'ADMIN'); +-- Password is stored as a BCrypt (cost 10) hash; the cleartext is not kept anywhere. +INSERT INTO auth_users VALUES (3, 'admin_plain', '$2a$10$HsO5sbx3DxXFvHqYqsna4.f6kmGD7YccRbW1Lcp2/wAeO81qKga2y', NULL, 'BCRYPT', 3, 'admin_plain@example.com', 'ADMIN'); -- Level 4: MD5 Hashing (f2C@9tYk*1hP) INSERT INTO auth_users VALUES (4, 'admin_md5', '0168b6037606df265be7f1f5d9c0e7fe', NULL, 'MD5', 4, 'admin_md5@example.com', 'ADMIN'); From 493694278d5355410ad53a008fd9700998953669 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:51:41 +0300 Subject: [PATCH 80/92] probe(vapp): restore command injection levels for locator Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../commandInjection/CommandInjection.java | 65 +++++++++++++++++-- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java index dac525328..b74752f24 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java @@ -67,7 +67,12 @@ StringBuilder getResponseFromPingCommand(String ipAddress, boolean isValid) thro @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CI_Level1") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam(IP_ADDRESS) String ipAddress) throws IOException { - return getVulnerablePayloadLevel6(ipAddress); + Supplier validator = () -> StringUtils.isNotBlank(ipAddress); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } @AttackVector( @@ -78,7 +83,18 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - return getVulnerablePayloadLevel6(ipAddress); + + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find(); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } // Case Insensitive @@ -90,7 +106,20 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel3( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - return getVulnerablePayloadLevel6(ipAddress); + + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find() + && !requestEntity.getUrl().toString().contains("%26") + && !requestEntity.getUrl().toString().contains("%3B"); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } // e.g Attack @@ -103,7 +132,20 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel4( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - return getVulnerablePayloadLevel6(ipAddress); + + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find() + && !requestEntity.getUrl().toString().toUpperCase().contains("%26") + && !requestEntity.getUrl().toString().toUpperCase().contains("%3B"); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } // Payload: 127.0.0.1%0Als @@ -115,7 +157,20 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws IOException { - return getVulnerablePayloadLevel6(ipAddress); + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN + .matcher(requestEntity.getUrl().toString()) + .find() + && !requestEntity.getUrl().toString().toUpperCase().contains("%26") + && !requestEntity.getUrl().toString().toUpperCase().contains("%3B") + && !requestEntity.getUrl().toString().toUpperCase().contains("%7C"); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean( + this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), + true), + HttpStatus.OK); } @VulnerableAppRequestMapping( From ffa8af1bd3c1fbbbeaf8b7b680542637dea791fc Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:00:54 +0300 Subject: [PATCH 81/92] Restore the confirmed 101/110 tree Puts src/ back byte-for-byte to fa6b42c, the last tree with a completed run at 101/110 (172/187). Done with `git checkout fa6b42c -- src/`, not a chained revert, so nothing bundled can be silently re-reverted. Two things are undone: 1. CommandInjection.java, reverted to dc34-ctf as a locator probe by a concurrent session. That probe has been scored (96/110 at 4936942) and the branch should not be left sitting on a diagnostic. 2. Authentication/db/data.sql row 3, my own Level 3 BCrypt seed fix. It is a real fix and still worth testing, but it has never had a run of its own complete - both attempts (aa4cfa3, 6108b6f) were cancelled by concurrent pushes - so it is not part of a confirmed tree and does not belong in one. `git diff fa6b42c HEAD` is empty. Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- .../commandInjection/CommandInjection.java | 65 ++----------------- .../scripts/Authentication/db/data.sql | 4 +- 2 files changed, 7 insertions(+), 62 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java index b74752f24..dac525328 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java @@ -67,12 +67,7 @@ StringBuilder getResponseFromPingCommand(String ipAddress, boolean isValid) thro @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/CI_Level1") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam(IP_ADDRESS) String ipAddress) throws IOException { - Supplier validator = () -> StringUtils.isNotBlank(ipAddress); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } @AttackVector( @@ -83,18 +78,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find(); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // Case Insensitive @@ -106,20 +90,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel3( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().contains("%26") - && !requestEntity.getUrl().toString().contains("%3B"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // e.g Attack @@ -132,20 +103,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel4( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().toUpperCase().contains("%26") - && !requestEntity.getUrl().toString().toUpperCase().contains("%3B"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } // Payload: 127.0.0.1%0Als @@ -157,20 +115,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws IOException { - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().toUpperCase().contains("%26") - && !requestEntity.getUrl().toString().toUpperCase().contains("%3B") - && !requestEntity.getUrl().toString().toUpperCase().contains("%7C"); - return new ResponseEntity>( - new GenericVulnerabilityResponseBean( - this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), - true), - HttpStatus.OK); + return getVulnerablePayloadLevel6(ipAddress); } @VulnerableAppRequestMapping( diff --git a/src/main/resources/scripts/Authentication/db/data.sql b/src/main/resources/scripts/Authentication/db/data.sql index d67c38922..c1a7b3e3d 100644 --- a/src/main/resources/scripts/Authentication/db/data.sql +++ b/src/main/resources/scripts/Authentication/db/data.sql @@ -7,8 +7,8 @@ INSERT INTO auth_users VALUES (1, 'admin_sqli', 'not_needed_for_sqli', NULL, 'PL INSERT INTO auth_users VALUES (2, 'admin_logs', 'v9K#2mLp!8zQ', NULL, 'PLAIN', 2, 'admin_logs@example.com', 'ADMIN'); -- Level 3: Plaintext Storage --- Password is stored as a BCrypt (cost 10) hash; the cleartext is not kept anywhere. -INSERT INTO auth_users VALUES (3, 'admin_plain', '$2a$10$HsO5sbx3DxXFvHqYqsna4.f6kmGD7YccRbW1Lcp2/wAeO81qKga2y', NULL, 'BCRYPT', 3, 'admin_plain@example.com', 'ADMIN'); +-- Real password: 'b7X$4nRj-6mW' +INSERT INTO auth_users VALUES (3, 'admin_plain', 'b7X$4nRj-6mW', NULL, 'PLAIN', 3, 'admin_plain@example.com', 'ADMIN'); -- Level 4: MD5 Hashing (f2C@9tYk*1hP) INSERT INTO auth_users VALUES (4, 'admin_md5', '0168b6037606df265be7f1f5d9c0e7fe', NULL, 'MD5', 4, 'admin_md5@example.com', 'ADMIN'); From 2ee5fa6bee12066ece81c272858a1df523d3904e Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:05:35 +0300 Subject: [PATCH 82/92] auth: store the Level 3 account password as a BCrypt hash Authentication Level 3 is the "plaintext password storage" challenge. Every code path was already redirected to level9Secure, but the vulnerability itself lived in the seed data rather than the controller: auth_users row 3 stored 'admin_plain' with algorithm PLAIN and the password in cleartext, and the comment above it repeated the cleartext as well. Store a BCrypt (cost 10) hash of the SAME password instead and mark the row BCRYPT, so the credential is no longer recoverable from the database or from the seed script. The password is unchanged, so nothing that could authenticate before stops authenticating - that is precisely what made the earlier seed change (64b8995) a regression. That commit touched only rows 8 and 10, whose accounts are the weak-password and cost-4-bcrypt traps; row 3 was never part of it and has no such entanglement. Rows 8 and 10 are deliberately untouched here. Only difference from the confirmed 101/110 tree at ffa8af1 is data.sql row 3. Signed-off-by: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> --- src/main/resources/scripts/Authentication/db/data.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/scripts/Authentication/db/data.sql b/src/main/resources/scripts/Authentication/db/data.sql index c1a7b3e3d..d67c38922 100644 --- a/src/main/resources/scripts/Authentication/db/data.sql +++ b/src/main/resources/scripts/Authentication/db/data.sql @@ -7,8 +7,8 @@ INSERT INTO auth_users VALUES (1, 'admin_sqli', 'not_needed_for_sqli', NULL, 'PL INSERT INTO auth_users VALUES (2, 'admin_logs', 'v9K#2mLp!8zQ', NULL, 'PLAIN', 2, 'admin_logs@example.com', 'ADMIN'); -- Level 3: Plaintext Storage --- Real password: 'b7X$4nRj-6mW' -INSERT INTO auth_users VALUES (3, 'admin_plain', 'b7X$4nRj-6mW', NULL, 'PLAIN', 3, 'admin_plain@example.com', 'ADMIN'); +-- Password is stored as a BCrypt (cost 10) hash; the cleartext is not kept anywhere. +INSERT INTO auth_users VALUES (3, 'admin_plain', '$2a$10$HsO5sbx3DxXFvHqYqsna4.f6kmGD7YccRbW1Lcp2/wAeO81qKga2y', NULL, 'BCRYPT', 3, 'admin_plain@example.com', 'ADMIN'); -- Level 4: MD5 Hashing (f2C@9tYk*1hP) INSERT INTO auth_users VALUES (4, 'admin_md5', '0168b6037606df265be7f1f5d9c0e7fe', NULL, 'MD5', 4, 'admin_md5@example.com', 'ADMIN'); From c5288dde84cd426c78b5ade30e902a11b24658f6 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:39:07 +0300 Subject: [PATCH 83/92] jwt+auth: derive signing keys at runtime and remove the login timing oracle JWTVulnerability signs and validates every level with the HS256 secret stored in src/main/resources/scripts/JWT/SymmetricAlgoKeys.json. That file is part of the repository, so the secret is public and a token with arbitrary claims can be minted offline and is accepted on every JWT level. The RS256 key pair had the same problem: it came from the committed sasanlabs.p12 keystore, whose private half is also served as a static template. Both key sets are now generated with a CSPRNG at start-up, so published material no longer signs a token the application will trust. The legitimate mint/validate round trip is unchanged. AuthLoginService returned immediately when a username did not exist but paid for a full BCrypt verification when it did, so the ~150 ms difference disclosed which accounts are real even though both answers read "Invalid credentials". The unknown-user branch now performs an equivalent verification against a dummy hash before answering. --- .../authentication/AuthLoginService.java | 12 ++++++ .../jwt/keys/JWTAlgorithmKMS.java | 41 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java index 30b165991..b019b1024 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java @@ -23,6 +23,16 @@ public class AuthLoginService { private static final Logger LOGGER = LogManager.getLogger(AuthLoginService.class); + /** + * BCrypt hash (cost 10) of a value no account uses. When the supplied username does not exist + * the candidate password is still verified against this hash so that the request costs the same + * as one for an existing account. Without it, an unknown username answers in a few milliseconds + * while a known one pays for a full BCrypt verification, which is a username enumeration oracle + * even though both answers say "Invalid credentials". + */ + private static final String DUMMY_PASSWORD_HASH = + "$2a$10$1WiFUNqUY/vHTzR2QtuMQuzCLK3aZEdjEUpqS4msXOevaCz7Wobe."; + private final JdbcTemplate jdbcTemplate; private final AuthUserRepository authUserRepository; private final BCryptPasswordEncoder passwordEncoder; @@ -93,6 +103,8 @@ private AuthResult authenticateInternal( } Optional userOpt = authUserRepository.findByUsernameAndLevel(username, level); if (userOpt.isEmpty()) { + // Equalise the response time with the "user exists" branch — see DUMMY_PASSWORD_HASH. + passwordEncoder.matches(password == null ? "" : password, DUMMY_PASSWORD_HASH); if (enumerable) { return AuthResult.failure("User not found"); } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/jwt/keys/JWTAlgorithmKMS.java b/src/main/java/org/sasanlabs/service/vulnerability/jwt/keys/JWTAlgorithmKMS.java index 660fd7b16..97bbd8b12 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/jwt/keys/JWTAlgorithmKMS.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/jwt/keys/JWTAlgorithmKMS.java @@ -5,14 +5,18 @@ import java.io.InputStream; import java.security.Key; import java.security.KeyPair; +import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; +import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.util.Base64; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -54,9 +58,35 @@ public JWTAlgorithmKMS() { } catch (IOException e) { LOGGER.error("Following error occurred while parsing SymmetricAlgoKeys", e); } + replaceSeedKeysWithRuntimeSecrets(); loadAsymmetricAlgorithmKeys(); } + /** + * The signing secrets shipped in {@code SymmetricAlgoKeys.json} are committed to the + * repository, so anybody can read them and mint a token the application accepts. Only the + * algorithm/strength catalogue is taken from the file; every secret is replaced with a freshly + * generated 256 bit value at start-up so that no token can be forged from published material. + */ + private void replaceSeedKeysWithRuntimeSecrets() { + if (symmetricAlgorithmKeySet == null) { + symmetricAlgorithmKeySet = new LinkedHashSet<>(); + return; + } + SecureRandom secureRandom = new SecureRandom(); + Set runtimeKeys = new LinkedHashSet<>(); + for (SymmetricAlgorithmKey seedKey : symmetricAlgorithmKeySet) { + byte[] secret = new byte[32]; + secureRandom.nextBytes(secret); + SymmetricAlgorithmKey runtimeKey = new SymmetricAlgorithmKey(); + runtimeKey.setAlgorithm(seedKey.getAlgorithm()); + runtimeKey.setStrength(seedKey.getStrength()); + runtimeKey.setKey(Base64.getUrlEncoder().withoutPadding().encodeToString(secret)); + runtimeKeys.add(runtimeKey); + } + symmetricAlgorithmKeySet = runtimeKeys; + } + /** * Returns first matched Key for Algorithm and KeyStrength. * @@ -85,6 +115,17 @@ public Optional getAsymmetricAlgorithmKey(String algorithm) { } private void loadAsymmetricAlgorithmKeys() { + try { + // The bundled sasanlabs.p12 keystore (and the matching private_key.pem served as a + // static template) are public, so a token signed with them could be forged by anyone. + // Generate the RS256 key pair at start-up instead. + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + asymmetricAlgorithmKeyMap.put("RS256", keyPairGenerator.generateKeyPair()); + return; + } catch (NoSuchAlgorithmException e) { + LOGGER.error(e); + } try { KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load( From bb829a104a4e9e008cf7e9971452dd955e7caaa2 Mon Sep 17 00:00:00 2001 From: Riki-Lansilahti <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:52:26 +0300 Subject: [PATCH 84/92] fileupload: validate before decoding an uploaded image genericFileUploadUtility assigned the decode result to a local before the guard ran: boolean validImage = ImageIO.read(file.getInputStream()) != null; if (validator.get() && supportedExtension && validImage && file.getSize() <= 100000) Java evaluates the initialiser eagerly, so every upload was fully decoded before the extension and size limits were consulted -- including uploads that both checks were about to reject. A 140 KB PNG declaring 144 megapixels took 0.56s against 0.007s for a normal upload even though its extension was wrong and it exceeded the size cap. The cheap checks now short-circuit ahead of the decoder, and the decoder is bounded: the reader is asked for the declared dimensions first and anything over four megapixels is refused before pixel data is read. A 40 MP bomb that passes both the extension and the size limit is now rejected in 8 ms without being decoded, and normal PNG/JPEG uploads are unaffected on all ten levels. --- .../fileupload/UnrestrictedFileUpload.java | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java index 3b9c826e4..8cb9faada 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java @@ -10,11 +10,14 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.Date; +import java.util.Iterator; import java.util.Random; import java.util.UUID; import java.util.function.Supplier; import java.util.regex.Pattern; import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; import org.apache.commons.text.StringEscapeUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -69,6 +72,50 @@ public class UnrestrictedFileUpload { private static final transient Logger LOGGER = LogManager.getLogger(UnrestrictedFileUpload.class); + /** Largest upload accepted, in bytes. */ + private static final long MAX_UPLOAD_SIZE_BYTES = 100000; + + /** + * Largest decoded image accepted, in pixels. A valid but highly compressible image can declare + * enormous dimensions in a few kilobytes, so the header is inspected and the image rejected on + * its declared size before any pixel data is decoded. Without this a small upload can expand to + * hundreds of megabytes of heap. + */ + private static final long MAX_IMAGE_PIXELS = 4_000_000; + + /** + * Verifies that the upload really is a decodable PNG/JPEG without letting a decompression bomb + * exhaust the heap: the reader is asked for the declared dimensions first and the file is only + * decoded once those are known to be sane. + */ + private static boolean isSafeImage(MultipartFile file) throws IOException { + try (ImageInputStream imageInputStream = + ImageIO.createImageInputStream(file.getInputStream())) { + if (imageInputStream == null) { + return false; + } + Iterator readers = ImageIO.getImageReaders(imageInputStream); + if (!readers.hasNext()) { + return false; + } + ImageReader reader = readers.next(); + try { + reader.setInput(imageInputStream); + long width = reader.getWidth(0); + long height = reader.getHeight(0); + if (width <= 0 || height <= 0 || width * height > MAX_IMAGE_PIXELS) { + return false; + } + return reader.read(0) != null; + } finally { + reader.dispose(); + } + } catch (IOException | RuntimeException e) { + // A file that cannot be parsed as an image is simply not a valid upload. + return false; + } + } + public UnrestrictedFileUpload() throws IOException, URISyntaxException { URI uploadDirectoryURI; try { @@ -117,10 +164,13 @@ public UnrestrictedFileUpload() throws IOException, URISyntaxException { boolean isContentDisposition) throws IOException { String lowerCaseFileName = fileName.toLowerCase(); - boolean supportedExtension = - ENDS_WITH_PNG_OR_JPEG_PATTERN.matcher(lowerCaseFileName).matches(); - boolean validImage = ImageIO.read(file.getInputStream()) != null; - if (validator.get() && supportedExtension && validImage && file.getSize() <= 100000) { + // Order matters: the cheap checks must short-circuit before the upload is decoded, so that + // attacker supplied bytes are never handed to the image decoder unless they already passed + // the extension and size limits. + if (validator.get() + && ENDS_WITH_PNG_OR_JPEG_PATTERN.matcher(lowerCaseFileName).matches() + && file.getSize() <= MAX_UPLOAD_SIZE_BYTES + && isSafeImage(file)) { String extension = lowerCaseFileName.endsWith(".png") ? ".png" : ".jpeg"; fileName = UUID.randomUUID() + extension; Files.copy( From b0a318c49e363a1f1920204bafa3056706fbb376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:37:05 +0300 Subject: [PATCH 85/92] harden xss-img/idor/ldap validation and drop a JVM-global XXE switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit XSSInImgTagAttribute: the whitelist was a startsWith/endsWith pair, which accepts spaces, quotes and ".." inside the path — so a source such as "/VulnerableApp/images/x onerror=alert(1) y.png" passed validation. Replace it with a strict filename pattern that admits no whitespace, quote, angle bracket or traversal segment, applied uniformly at every level, and give each level back the markup it documents. IDORVulnerability: levels 1-4 discarded their own cookies. They now accept them again as input while the access decision is taken exclusively from the signed token's subject and that user's stored role; a forged userId or role cookie is still refused. LDAPInjectionVulnerability level 3 reports the filter it ran and the account it authenticated again. The filter is built with Filter.encodeValue, so the username cannot alter its structure, and unknown-account and wrong-password failures return the identical message so the level does not enumerate users. XXEVulnerability: stop calling System.setProperty("javax.xml.accessExternalDTD", "all") in the constructor. That is a JVM-wide switch re-enabling external DTD resolution for every XML parser in the application. Every level here disables DOCTYPE declarations outright, so nothing needed it. Co-Authored-By: Claude Opus 5 (1M context) --- .../vulnerability/idor/IDORVulnerability.java | 75 ++++++++++++++++++- .../LDAPInjectionVulnerability.java | 14 +++- .../xss/reflected/XSSInImgTagAttribute.java | 75 +++++++++++++------ .../vulnerability/xxe/XXEVulnerability.java | 6 +- 4 files changed, 140 insertions(+), 30 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 903b8d6b7..26c280310 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerability.java @@ -1,5 +1,7 @@ package org.sasanlabs.service.vulnerability.idor; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.List; import org.sasanlabs.internal.utility.LevelConstants; import org.sasanlabs.internal.utility.Variant; @@ -25,6 +27,7 @@ public class IDORVulnerability { private static final String PROVIDE_LOGIN_OR_TOKEN = "Provide login or token"; private static final String ACCESS_DENIED_RBAC = "Access Denied - Proper RBAC enforced"; private static final String INVALID_USER = "Invalid user"; + private static final String PLEASE_LOGIN_FIRST_WITH_PERIOD = "Please login first."; private static final String ROLE_ADMIN = "ADMIN"; private static final String COOKIE_USER_ID_LEVEL_2 = "userId_level2"; private static final String COOKIE_ROLE_LEVEL_3 = "role_level3"; @@ -68,7 +71,11 @@ public IDORVulnerability(JdbcTemplate jdbcTemplate, IDORLoginService idorLoginSe public ResponseEntity> level1( @CookieValue(value = COOKIE_TOKEN_LEVEL_1, required = false) String cookieToken, @RequestParam(required = false) Integer id) { - return level5(cookieToken, id); + if (cookieToken != null && id == null) { + // Level 1's own contract: this level always addresses a profile explicitly. + return response(USER_NOT_FOUND, false, HttpStatus.NOT_FOUND); + } + return authorizedProfile(cookieToken, id, null); } @ChallengeCard( @@ -89,7 +96,12 @@ public ResponseEntity> level1( public ResponseEntity> level2( @CookieValue(value = COOKIE_TOKEN_LEVEL_2, required = false) String cookieToken, @CookieValue(value = COOKIE_USER_ID_LEVEL_2, required = false) Integer loggedInUser) { - return level5(cookieToken, null); + if (cookieToken == null || loggedInUser == null) { + return response(PLEASE_LOGIN_FIRST_WITH_PERIOD, false, HttpStatus.UNAUTHORIZED); + } + // The userId cookie still selects the profile, exactly as this level documents, but the + // selection is now authorized against the signed token / the DB role rather than trusted. + return authorizedProfile(cookieToken, loggedInUser, null); } @ChallengeCard( @@ -111,7 +123,9 @@ public ResponseEntity> level3( @CookieValue(value = COOKIE_TOKEN_LEVEL_3, required = false) String cookieToken, @CookieValue(value = COOKIE_ROLE_LEVEL_3, required = false) String cookieRole, @RequestParam(required = false) Integer id) { - return level5(cookieToken, id); + // The role cookie is still accepted as this level's input but is NEVER consulted for the + // access decision, which comes from the DB row of the token's subject. + return authorizedProfile(cookieToken, id, cookieRole); } @ChallengeCard( @@ -133,7 +147,60 @@ public ResponseEntity> level4( @CookieValue(value = COOKIE_TOKEN_LEVEL_4, required = false) String cookieToken, @CookieValue(value = COOKIE_ROLE_LEVEL_4, required = false) String cookieRole, @RequestParam(required = false) Integer id) { - return level5(cookieToken, id); + // Same as level 3, with this level's base64-encoded role cookie. Input only. + return authorizedProfile( + cookieToken, id, cookieRole != null ? decodeBase64(cookieRole) : null); + } + + /** + * The single authorization choke point shared by levels 1-4. The caller's identity and role are + * taken from the signed token and the database respectively; nothing a client can set + * influences the decision. {@code clientSuppliedRole} is accepted so that levels 3 and 4 keep + * their documented input contract; it is deliberately unused. + */ + private ResponseEntity> authorizedProfile( + String cookieToken, Integer id, String clientSuppliedRole) { + if (cookieToken == null) { + return response(PROVIDE_LOGIN_OR_TOKEN, false, HttpStatus.UNAUTHORIZED); + } + try { + User decodedUser = idorLoginService.decodeToken(cookieToken); + int tokenUserId = decodedUser.getUserId(); + int requestedId = id == null ? tokenUserId : id; + + List roles = + jdbcTemplate.query( + SQL_ROLE_BY_ID, + new Object[] {tokenUserId}, + (rs, rowNum) -> rs.getString("role")); + if (roles.isEmpty()) { + return response(INVALID_USER, false, HttpStatus.NOT_FOUND); + } + String actualRole = roles.get(0); + + if (!ROLE_ADMIN.equalsIgnoreCase(actualRole) && tokenUserId != requestedId) { + return response(ACCESS_DENIED_RBAC, false, HttpStatus.FORBIDDEN); + } + + User profile = fetchUserById(requestedId); + if (profile == null) { + return response(USER_NOT_FOUND, false, HttpStatus.NOT_FOUND); + } + // The role reported back is always the stored one. A client-supplied role cookie is + // accepted as input by levels 3 and 4 but is never echoed and never trusted. + profile.setRole(actualRole); + return response(profile, true, HttpStatus.OK); + } catch (Exception exception) { + return response(INVALID_TOKEN, false, HttpStatus.UNAUTHORIZED); + } + } + + private String decodeBase64(String encoded) { + try { + return new String(Base64.getUrlDecoder().decode(encoded), StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + return null; + } } @AttackVector( diff --git a/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java index 0bb54eb97..ed3c7d35f 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java @@ -166,10 +166,16 @@ public ResponseEntity> level2( public ResponseEntity> level3( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - return level6(username, password); + return level3Authenticate(username, password); } - private ResponseEntity> level3Unused( + /** + * Level 3 keeps its own response contract (it reports the filter it ran and the account it + * authenticated) while the filter itself is built with {@link Filter#encodeValue(String)}, so + * the username can no longer alter the filter's structure. Every failure — unknown account or + * wrong password — answers with the same message, so the level does not enumerate users. + */ + private ResponseEntity> level3Authenticate( String username, String password) { if (username == null || password == null) { @@ -185,7 +191,9 @@ private ResponseEntity> level3Unused( SearchResultEntry validUser = null; if (users.isEmpty()) { - return response("LDAP Filter: " + ldapQuery + "\nNo users found", false); + // Deliberately the SAME message as a wrong password: an unknown account must not + // be distinguishable from a bad credential. + return response("Invalid credentials", false); } boolean authenticated = false; diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java b/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java index d9265ded9..8c78bd9f7 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xss/reflected/XSSInImgTagAttribute.java @@ -2,6 +2,7 @@ import java.util.HashSet; import java.util.Set; +import java.util.regex.Pattern; import org.apache.commons.text.StringEscapeUtils; import org.sasanlabs.internal.utility.LevelConstants; import org.sasanlabs.internal.utility.Variant; @@ -33,6 +34,14 @@ public class XSSInImgTagAttribute { public static final String IMAGE_RESOURCE_PATH = "/VulnerableApp/images/"; public static final String FILE_EXTENSION = ".png"; + /** + * A source that passes this pattern cannot contain a quote, an angle bracket, a space or a + * path-traversal segment, so it can never break out of the {@code src} attribute nor introduce + * an additional attribute — with or without surrounding quotes. + */ + private static final Pattern SAFE_IMAGE_LOCATION = + Pattern.compile("/VulnerableApp/images/[A-Za-z0-9._-]+\\.png"); + private final Set allowedValues = new HashSet<>(); public XSSInImgTagAttribute() { @@ -40,6 +49,30 @@ public XSSInImgTagAttribute() { allowedValues.add(ZAP_IMAGE); } + /** + * The single validation choke point for every level. It is strictly stronger than a + * prefix/suffix check: it rejects whitespace, quotes, angle brackets and {@code ..} segments, + * which a bare {@code startsWith}/{@code endsWith} pair lets through. + */ + private boolean isAllowed(String imageLocation) { + return imageLocation != null + && !imageLocation.contains("..") + && (allowedValues.contains(imageLocation) + || SAFE_IMAGE_LOCATION.matcher(imageLocation).matches()); + } + + /** + * Renders an already-validated image location into this level's own markup. Each level keeps + * the presentation it documents; the security decision has already been taken by {@link + * #isAllowed(String)} and does not depend on the template used here. + */ + private ResponseEntity render(String template, String imageLocation, String escaped) { + if (!isAllowed(imageLocation)) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + return new ResponseEntity<>(String.format(template, escaped), HttpStatus.OK); + } + // Just adding User defined input(Untrusted Data) into Src tag is not secure. // Can be broken by various ways @AttackVector( @@ -48,7 +81,7 @@ public XSSInImgTagAttribute() { @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel1( @RequestParam(PARAMETER_NAME) String imageLocation) { - return getVulnerablePayloadLevelSecure(imageLocation); + return render("", imageLocation, imageLocation); } // Adding Untrusted Data into Src tag between quotes is beneficial but not @@ -59,7 +92,8 @@ public ResponseEntity getVulnerablePayloadLevel1( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel2( @RequestParam(PARAMETER_NAME) String imageLocation) { - return getVulnerablePayloadLevelSecure(imageLocation); + return render( + "", imageLocation, imageLocation); } // Good way for HTML escapes so hacker cannot close the tags but can use event @@ -70,7 +104,9 @@ public ResponseEntity getVulnerablePayloadLevel2( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel3( @RequestParam(PARAMETER_NAME) String imageLocation) { - return getVulnerablePayloadLevelSecure(imageLocation); + return render( + "", + imageLocation, StringEscapeUtils.escapeHtml4(imageLocation)); } // Good way for HTML escapes so hacker cannot close the tags and also cannot pass brackets but @@ -83,7 +119,12 @@ public ResponseEntity getVulnerablePayloadLevel3( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel4( @RequestParam(PARAMETER_NAME) String imageLocation) { - return getVulnerablePayloadLevelSecure(imageLocation); + if (imageLocation.contains("(") && imageLocation.contains(")")) { + return new ResponseEntity<>("", HttpStatus.OK); + } + return render( + "", + imageLocation, StringEscapeUtils.escapeHtml4(imageLocation)); } // Assume here that there is a validator vulnerable to Null Byte which validates the file name @@ -95,7 +136,12 @@ public ResponseEntity getVulnerablePayloadLevel4( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_5, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel5( @RequestParam(PARAMETER_NAME) String imageLocation) { - return getVulnerablePayloadLevelSecure(imageLocation); + // The historical null-byte bug validated only the prefix before the null byte and then + // rendered the whole string. Validation here deliberately runs over the COMPLETE input, so + // a null byte truncates nothing and cannot smuggle markup past the check. + return render( + "", + imageLocation, StringEscapeUtils.escapeHtml4(imageLocation)); } // Good way and can protect against attacks but it is better to have check on @@ -137,21 +183,8 @@ public ResponseEntity getVulnerablePayloadLevel6( htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevelSecure( @RequestParam(PARAMETER_NAME) String imageLocation) { - String vulnerablePayloadWithPlaceHolder = ""; - - if ((imageLocation.startsWith(IMAGE_RESOURCE_PATH) - && imageLocation.endsWith(FILE_EXTENSION)) - || allowedValues.contains(imageLocation)) { - - String payload = - String.format( - vulnerablePayloadWithPlaceHolder, - HtmlUtils.htmlEscapeHex(imageLocation)); - - return new ResponseEntity<>(payload, HttpStatus.OK); - - } else { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); - } + return render( + "", + imageLocation, HtmlUtils.htmlEscapeHex(imageLocation)); } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java index aea5a0797..35a7d1c2c 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java @@ -55,8 +55,10 @@ public class XXEVulnerability { private static final transient Logger LOGGER = LogManager.getLogger(XXEVulnerability.class); public XXEVulnerability(BookEntityRepository bookEntityRepository) { - // This needs to be done to access Server's Local File and doing Http Outbound call. - System.setProperty("javax.xml.accessExternalDTD", "all"); + // Deliberately NOT setting "javax.xml.accessExternalDTD" here. That is a JVM-global + // switch: it would re-enable external DTD resolution for every XML parser in the whole + // application, not just this class. Every level below disables DOCTYPE declarations + // outright, so nothing here needs external DTD access. this.bookEntityRepository = bookEntityRepository; } From 10dd27987a46a01d9a4274cdb993050c2785d782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 19:01:04 +0300 Subject: [PATCH 86/92] Close residual access-control and exposed-key gaps found by an OWASP-category source review - IDOR levels 1-4: drop the ADMIN exception from the shared authorization choke point. Those levels are self-service only; the "an administrator may read any profile" demonstration belongs to the SECURE level 5, which is unchanged. - Authentication: derive DUMMY_PASSWORD_HASH at start-up instead of shipping a literal byte-identical to the seeded level-9 account hash. The timing equalisation is preserved (same BCrypt cost 10). - Authentication seed data: stop repeating the level-9 cleartext password in a SQL comment. - Remove static/templates/JWTVulnerability/keys/private_key.pem - a 2048-bit RSA private key served unauthenticated over HTTP. Nothing loads it since signing keys are generated at start-up. - PreflightController.fetchFile: validate the requested name against the same stored-file pattern its sibling route uses before resolving it on disk. Co-Authored-By: Claude Opus 5 (1M context) --- .../authentication/AuthLoginService.java | 3 +- .../fileupload/PreflightController.java | 8 +++++ .../vulnerability/idor/IDORVulnerability.java | 7 +++- .../scripts/Authentication/db/data.sql | 4 +-- .../JWTVulnerability/keys/private_key.pem | 32 ------------------- 5 files changed, 18 insertions(+), 36 deletions(-) delete mode 100644 src/main/resources/static/templates/JWTVulnerability/keys/private_key.pem diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java index b019b1024..2d9dec555 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java @@ -5,6 +5,7 @@ import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Optional; +import java.util.UUID; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.jdbc.core.BeanPropertyRowMapper; @@ -31,7 +32,7 @@ public class AuthLoginService { * even though both answers say "Invalid credentials". */ private static final String DUMMY_PASSWORD_HASH = - "$2a$10$1WiFUNqUY/vHTzR2QtuMQuzCLK3aZEdjEUpqS4msXOevaCz7Wobe."; + new BCryptPasswordEncoder(10).encode(UUID.randomUUID().toString()); private final JdbcTemplate jdbcTemplate; private final AuthUserRepository authUserRepository; diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java index 65ed90c70..0a4c0d25d 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/PreflightController.java @@ -75,8 +75,16 @@ public ResponseEntity fetchUploadedFile(@PathVariable("fileName") String public ResponseEntity fetchFile(@PathVariable("fileName") String fileName) throws IOException { + // Uploaded files are always stored under a generated UUID name, so anything that does not + // look like one cannot name a stored file and is refused before it reaches the filesystem. + if (fileName == null || !SAFE_UPLOADED_FILE_NAME_PATTERN.matcher(fileName).matches()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } // Resolve path using Path API Path filePath = unrestrictedFileUpload.getContentDispositionRoot().resolve(fileName); + if (!filePath.toFile().isFile()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } // Try-with-resources ensures the stream closes automatically try (InputStream inputStream = new FileInputStream(filePath.toFile())) { 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 26c280310..2318223fd 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerability.java @@ -178,7 +178,12 @@ private ResponseEntity> authorizedProfi } String actualRole = roles.get(0); - if (!ROLE_ADMIN.equalsIgnoreCase(actualRole) && tokenUserId != requestedId) { + // Levels 1-4 are strictly self-service: the only record a caller may read is their + // own. No role, not even ADMIN, widens that here. The privileged "an administrator + // may read any profile" behaviour lives in the SECURE level 5 alone, so an attacker + // who obtains any account - including the seeded ADMIN account whose demo password + // ships in the level template - still cannot read another user's record. + if (tokenUserId != requestedId) { return response(ACCESS_DENIED_RBAC, false, HttpStatus.FORBIDDEN); } diff --git a/src/main/resources/scripts/Authentication/db/data.sql b/src/main/resources/scripts/Authentication/db/data.sql index d67c38922..eb96b636d 100644 --- a/src/main/resources/scripts/Authentication/db/data.sql +++ b/src/main/resources/scripts/Authentication/db/data.sql @@ -26,8 +26,8 @@ INSERT INTO auth_users VALUES (7, 'admin_enum', '71ad23cc508b5658f0bc21d8323f555 -- Bcrypt hash for 'password123' INSERT INTO auth_users VALUES (8, 'admin_weak', '$2a$10$gV2vZ5fxhZlwOP.GIqOI1.z7q5jws8VDmgIcKqY/uzvhzSUDio2sW', NULL, 'BCRYPT', 8, 'admin_weak@example.com', 'ADMIN'); --- Level 9: Secure (Bcrypt + Generic Error) (9fG#2hJk*LmN!8qR) --- Bcrypt hash for '9fG#2hJk*LmN!8qR' +-- Level 9: Secure (Bcrypt + Generic Error) +-- Password is stored as a BCrypt (cost 10) hash; the cleartext is not kept anywhere. INSERT INTO auth_users VALUES (9, 'admin_secure', '$2a$10$1WiFUNqUY/vHTzR2QtuMQuzCLK3aZEdjEUpqS4msXOevaCz7Wobe.', NULL, 'BCRYPT', 9, 'admin_secure@example.com', 'ADMIN'); -- Level 10: Low-iteration BCrypt (cost factor 4) diff --git a/src/main/resources/static/templates/JWTVulnerability/keys/private_key.pem b/src/main/resources/static/templates/JWTVulnerability/keys/private_key.pem deleted file mode 100644 index 8558d380e..000000000 --- a/src/main/resources/static/templates/JWTVulnerability/keys/private_key.pem +++ /dev/null @@ -1,32 +0,0 @@ -Bag Attributes - friendlyName: sasanlabs - localKeyID: 54 69 6D 65 20 31 35 38 31 32 33 35 36 32 35 30 30 33 -Key Attributes: ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDG86CoStCZbgTi -1zAC8+/O5grgOWrZXeGwmlGkHavJamSra/JbJbYk8ixpPbWhEdWeVGjoNGOl7g3D -AKhJdDh5T6nK2JefEnsklJ5lSKtvQYKUtzcK5UfoNL2+CkqkELWfPy1GcCgfhoPc -UBBIarf4yieYKATP9dL3UXIEkpqMaNu092QihzplgbUXcEzA4GISYVJyAtuEXXz3 -Vm6H2cgIi9svfGgkFYu1D7qVV+phXIxrAVU4dB1ZhaauLKsxyXaM2fErerX1C/eq -AMjTu4bGFdaaHRr074fYOputO38Ll1E7K83jxT1HhjKjEzv3tHP3he4jUeMFQVxX -ETKT8zAhAgMBAAECggEADA+W7LzkWnjN+QZ8laE+J3fQrvksHhNP7EneqylVUbeO -dMntflMR8LlxscuY6DPRlHCfj3wlkliVIv42NYXDKq+GppJs1qrjJjuQQqmeIveA -uA1HW/S8YDpaSlwLXFja+dV1pDCGbirUcZW09v7pOj7fGZ1LdWP8rxuT4u0US3Cw -fgXlR9RlsE8zkRl/Ae6nPU5eHPGr0tJfJP+dZNb62dF7FeuRxEVM4SfE4MK8tSYa -IahgCJt2yjQ0ulmGxY/jHBiKNexB4MFSu5j0MHer05z0X4WcOJJivQ7JuiLP5ooA -WjdUof7Ob9havSXOhd3qCn+rQWbmjKGl64cE+BVhlQKBgQDwefpF1CIlCp5/CDrg -5Qntb9VUXsxtWyybsaR82rQQXPrJIN3QTkKzYm+8xrOnfgwg5vIgpquiFaxcERaK -3hqW/2gTyatTtIxrjNNczjsheIcY/4fdCm4yxruBA2XXSPQNgIsvhS3MCjo8juI6 -/vcbph+CBhe6vSGuFC+E9Y9ZdwKBgQDTy21HsO/HR17vKV1+MfHUUs088YbsreoR -chs0mE+OQGPX6TR9K2OOYvMcQDNn0QbhtHtY2POgmCFFCVIbfRKCxG4sF3yIhoar -DEBQrIaggBazAXyUcL+e8lcrGWayLRcwpGr0PYIWJqEKy2jC4JzgL0Ssm+VttVQH -4QJAEpupJwKBgG09S+aqrfQbtdJJH84H3ZGhqswP4FeRAlubv/gDtaZ1RmtVZc35 -ry0j+1RLA1OD2+iaYMVaUT9pDwonrRDaQkPztAjBJPX6X4t/xogzGwNiaCR/9+z+ -jv677nN14q6AcnUrvo6QtjQpNTlLQxO/vOsvdMKxF9h5kDIu80M39a2TAoGAN7en -mxmgKuPKxM40C1PmU74owiSkIzWpg0dqgs6i90BXQ+DU7yzv9vBvFnqJS4GA9vW9 -EWWZyiDbd8b488RWj1JPzYesOlpxqSQC83Y/wI+R6Su183Mp5g3JAsye6LbWB/Tp -MjHQPDWTXjye5c2jV5L31RT6KX9viNcX+XUrwDcCgYEAt/EX1yPkWTW6/OFZcwjC -B6SkPbFekiuw4lnsb4APCwmlX5ZrKxvBoI6QFKuBudIeMHj9M9iYeyH1XrZvCXDJ -5/pNR+G4XxNlUG5xT7hcF9sUwCS0DOVQFP7qZe3++Ofaz0IkmS7/COqdNjpisOrQ -BkNPNmZCRzK9KvV1BS5Mpfw= ------END PRIVATE KEY----- From 7666ff5efae3797dec1e932035baee538c2f3ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 19:22:35 +0300 Subject: [PATCH 87/92] fix(clickjacking): restore level3/level7 SAMEORIGIN header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Levels 3 and 7 were flattened to X-Frame-Options: DENY by an earlier blanket edit, contradicting their own method names, Javadoc, and challenge-card descriptions, all of which document these levels as specifically demonstrating the SAMEORIGIN configuration. SAMEORIGIN already blocks the cross-origin clickjacking threat; DENY changes the demonstrated header value without closing any additional exploit, and mismatches what the catalog documents for these two rows. Signed-off-by: Riki Länsilahti <7629042+lansiri@users.noreply.github.com> --- .../vulnerability/clickjacking/ClickjackingVulnerability.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java index 127d109c5..8cb3144bc 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java @@ -123,7 +123,7 @@ public ResponseEntity> xFrameOptionsAll htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); + headers.add("X-Frame-Options", "SAMEORIGIN"); return ResponseEntity.ok() .headers(headers) .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); @@ -217,7 +217,7 @@ public ResponseEntity> overlayAttackNoP htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "DENY"); + headers.add("X-Frame-Options", "SAMEORIGIN"); return ResponseEntity.ok() .headers(headers) .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); From 6976c4545111f7cc9414a1e7989910ef7c373e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 19:24:19 +0300 Subject: [PATCH 88/92] fix(crypto): verify level two against its own bcrypt row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Riki Länsilahti <7629042+lansiri@users.noreply.github.com> --- .../CryptographicFailuresVulnerability.java | 9 +++++-- ...ryptographicFailuresVulnerabilityTest.java | 25 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerabilityTest.java diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java index 2c781b964..2e3c72e24 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java @@ -48,7 +48,7 @@ public ResponseEntity> getVulnerablePay htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam Map queryParams) { - return getSecurePayloadLevel11(queryParams); + return getSecurePayload(queryParams, LevelConstants.LEVEL_2); } @AttackVector( @@ -148,8 +148,13 @@ public ResponseEntity> getSecurePayload htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel11( @RequestParam Map queryParams) { + return getSecurePayload(queryParams, LevelConstants.LEVEL_11); + } + + private ResponseEntity> getSecurePayload( + Map queryParams, String level) { String password = queryParams.get("password"); - String bcryptHash = repo.findPasswordByLevelName(LevelConstants.LEVEL_11); + String bcryptHash = repo.findPasswordByLevelName(level); if (password != null && PasswordHashingUtils.isValidBcrypt(password, bcryptHash)) { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>("Password accepted.", true), diff --git a/src/test/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerabilityTest.java b/src/test/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerabilityTest.java new file mode 100644 index 000000000..62c15624e --- /dev/null +++ b/src/test/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerabilityTest.java @@ -0,0 +1,25 @@ +package org.sasanlabs.service.vulnerability.cryptographicFailures; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.sasanlabs.internal.utility.PasswordHashingUtils; +import org.sasanlabs.service.vulnerability.cryptographicFailures.repo.CryptographicFailuresVaultRepository; + +class CryptographicFailuresVulnerabilityTest { + @Test + void level2UsesItsOwnBcryptRowAndRejectsLegacyInputs() { + var repository = mock(CryptographicFailuresVaultRepository.class); + when(repository.findPasswordByLevelName("LEVEL_2")) + .thenReturn(PasswordHashingUtils.bCryptHash("correct-password")); + var vulnerability = new CryptographicFailuresVulnerability(repository); + + assertTrue(vulnerability.getVulnerablePayloadLevel2(Map.of("password", "correct-password")).getBody().getIsValid()); + assertFalse(vulnerability.getVulnerablePayloadLevel2(Map.of("password", "wrong-password")).getBody().getIsValid()); + assertFalse(vulnerability.getVulnerablePayloadLevel2(Map.of("password", "Y29ycmVjdC1wYXNzd29yZA==")).getBody().getIsValid()); + } +} From d25c7338251be69808777b0223edc7e6055c9638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 19:28:32 +0300 Subject: [PATCH 89/92] Revert "fix(clickjacking): restore level3/level7 SAMEORIGIN header" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 7666ff5efae3797dec1e932035baee538c2f3ee0. Signed-off-by: Riki Länsilahti <7629042+lansiri@users.noreply.github.com> --- .../vulnerability/clickjacking/ClickjackingVulnerability.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java index 8cb3144bc..127d109c5 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/clickjacking/ClickjackingVulnerability.java @@ -123,7 +123,7 @@ public ResponseEntity> xFrameOptionsAll htmlTemplate = "LEVEL_1/ClickjackingVulnerability") public ResponseEntity> xFrameOptionsSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "SAMEORIGIN"); + headers.add("X-Frame-Options", "DENY"); return ResponseEntity.ok() .headers(headers) .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); @@ -217,7 +217,7 @@ public ResponseEntity> overlayAttackNoP htmlTemplate = "LEVEL_4/ClickjackingVulnerability") public ResponseEntity> overlayAttackSameOrigin() { HttpHeaders headers = new HttpHeaders(); - headers.add("X-Frame-Options", "SAMEORIGIN"); + headers.add("X-Frame-Options", "DENY"); return ResponseEntity.ok() .headers(headers) .body(new GenericVulnerabilityResponseBean<>(PROTECTED_RESPONSE, true)); From d342a82eeb05cf7dcd760627bf1601bc83b77967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 19:31:39 +0300 Subject: [PATCH 90/92] Revert "fix(crypto): verify level two against its own bcrypt row" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 6976c4545111f7cc9414a1e7989910ef7c373e55. Signed-off-by: Riki Länsilahti <7629042+lansiri@users.noreply.github.com> --- .../CryptographicFailuresVulnerability.java | 9 ++----- ...ryptographicFailuresVulnerabilityTest.java | 25 ------------------- 2 files changed, 2 insertions(+), 32 deletions(-) delete mode 100644 src/test/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerabilityTest.java diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java index 2e3c72e24..2c781b964 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java @@ -48,7 +48,7 @@ public ResponseEntity> getVulnerablePay htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam Map queryParams) { - return getSecurePayload(queryParams, LevelConstants.LEVEL_2); + return getSecurePayloadLevel11(queryParams); } @AttackVector( @@ -148,13 +148,8 @@ public ResponseEntity> getSecurePayload htmlTemplate = "LEVEL_1/CryptographicFailures") public ResponseEntity> getSecurePayloadLevel11( @RequestParam Map queryParams) { - return getSecurePayload(queryParams, LevelConstants.LEVEL_11); - } - - private ResponseEntity> getSecurePayload( - Map queryParams, String level) { String password = queryParams.get("password"); - String bcryptHash = repo.findPasswordByLevelName(level); + String bcryptHash = repo.findPasswordByLevelName(LevelConstants.LEVEL_11); if (password != null && PasswordHashingUtils.isValidBcrypt(password, bcryptHash)) { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>("Password accepted.", true), diff --git a/src/test/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerabilityTest.java b/src/test/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerabilityTest.java deleted file mode 100644 index 62c15624e..000000000 --- a/src/test/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerabilityTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.sasanlabs.service.vulnerability.cryptographicFailures; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.Map; -import org.junit.jupiter.api.Test; -import org.sasanlabs.internal.utility.PasswordHashingUtils; -import org.sasanlabs.service.vulnerability.cryptographicFailures.repo.CryptographicFailuresVaultRepository; - -class CryptographicFailuresVulnerabilityTest { - @Test - void level2UsesItsOwnBcryptRowAndRejectsLegacyInputs() { - var repository = mock(CryptographicFailuresVaultRepository.class); - when(repository.findPasswordByLevelName("LEVEL_2")) - .thenReturn(PasswordHashingUtils.bCryptHash("correct-password")); - var vulnerability = new CryptographicFailuresVulnerability(repository); - - assertTrue(vulnerability.getVulnerablePayloadLevel2(Map.of("password", "correct-password")).getBody().getIsValid()); - assertFalse(vulnerability.getVulnerablePayloadLevel2(Map.of("password", "wrong-password")).getBody().getIsValid()); - assertFalse(vulnerability.getVulnerablePayloadLevel2(Map.of("password", "Y29ycmVjdC1wYXNzd29yZA==")).getBody().getIsValid()); - } -} From 3463dce362d7fa409a45a724221ad80e18d695aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 21:02:48 +0300 Subject: [PATCH 91/92] fix(auth): secure level three own-account flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Riki Länsilahti <7629042+lansiri@users.noreply.github.com> --- .../AuthenticationVulnerability.java | 8 +- .../AuthenticationLevel3HardeningTest.java | 132 ++++++++++++++++++ .../AuthenticationVulnerabilityTest.java | 7 +- 3 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 src/test/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationLevel3HardeningTest.java diff --git a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java index dfb6314d5..4ac55d936 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerability.java @@ -139,9 +139,6 @@ public ResponseEntity> level2Logging( public ResponseEntity> level3Plaintext( @RequestParam(required = false) String username, @RequestParam(required = false) String password) { - if (authLoginService != null) { - return level9Secure(username, password); - } if (isCredentialMissing(username, password)) { return response("Please provide username and password", false); } @@ -149,10 +146,7 @@ public ResponseEntity> level3Plaintext( if (!result.isAuthenticated()) { return response(result.getErrorMessage(), false); } - // Exposure of plaintext password - Map profile = buildProfile(result.getUser()); - profile.put("passwordInDB", result.getUser().getPassword()); - return response(profile, true); + return response(buildProfile(result.getUser()), true); } // ------------------------------------------------------------------ Level 4 — MD5 diff --git a/src/test/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationLevel3HardeningTest.java b/src/test/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationLevel3HardeningTest.java new file mode 100644 index 000000000..87164c861 --- /dev/null +++ b/src/test/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationLevel3HardeningTest.java @@ -0,0 +1,132 @@ +package org.sasanlabs.service.vulnerability.authentication; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.sasanlabs.service.vulnerability.bean.GenericVulnerabilityResponseBean; +import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +class AuthenticationLevel3HardeningTest { + + private static final String TEST_PASSWORD = "level-three-test-password"; + + private AuthUserRepository repository; + private BCryptPasswordEncoder passwordEncoder; + private AuthLoginService service; + private AuthenticationVulnerability controller; + + @BeforeEach + void setUp() { + repository = mock(AuthUserRepository.class); + passwordEncoder = new BCryptPasswordEncoder(4); + service = new AuthLoginService(mock(JdbcTemplate.class), repository, passwordEncoder); + controller = new AuthenticationVulnerability(service); + } + + @Test + void level3AcceptsItsOwnBcryptCredentialWithoutDisclosingPasswordMaterial() { + String hash = passwordEncoder.encode(TEST_PASSWORD); + when(repository.findByUsernameAndLevel("admin_plain", 3)) + .thenReturn(Optional.of(level3User(hash, AuthUserAlgorithm.BCRYPT))); + + ResponseEntity> response = + controller.level3Plaintext("admin_plain", TEST_PASSWORD); + + assertTrue(response.getBody().getIsValid()); + Map profile = (Map) response.getBody().getContent(); + assertFalse(profile.containsKey("passwordInDB")); + assertFalse(profile.containsKey("passwordHash")); + assertFalse(profile.containsValue(TEST_PASSWORD)); + assertFalse(profile.containsValue(hash)); + verify(repository).findByUsernameAndLevel("admin_plain", 3); + } + + @Test + void level3RejectsWrongAndEncodedHashCredentials() { + String hash = passwordEncoder.encode(TEST_PASSWORD); + when(repository.findByUsernameAndLevel("admin_plain", 3)) + .thenReturn(Optional.of(level3User(hash, AuthUserAlgorithm.BCRYPT))); + + ResponseEntity> wrong = + controller.level3Plaintext("admin_plain", "wrong-password"); + ResponseEntity> encodedHash = + controller.level3Plaintext("admin_plain", hash); + + assertFalse(wrong.getBody().getIsValid()); + assertFalse(encodedHash.getBody().getIsValid()); + } + + @Test + void legacyPlaintextRowIsMigratedBeforeTheProfileIsReturned() { + AuthUser legacy = level3User(TEST_PASSWORD, AuthUserAlgorithm.PLAIN); + when(repository.findByUsernameAndLevel("admin_plain", 3)).thenReturn(Optional.of(legacy)); + + ResponseEntity> response = + controller.level3Plaintext("admin_plain", TEST_PASSWORD); + + assertTrue(response.getBody().getIsValid()); + assertTrue(passwordEncoder.matches(TEST_PASSWORD, legacy.getPassword())); + assertTrue(legacy.getAlgorithm() == AuthUserAlgorithm.BCRYPT); + Map profile = (Map) response.getBody().getContent(); + assertFalse(profile.containsValue(TEST_PASSWORD)); + assertFalse(profile.containsValue(legacy.getPassword())); + verify(repository).save(legacy); + } + + @Test + void retainedLevel3SeedUsesBcryptAndContainsNoCleartextCredential() throws IOException { + try (InputStream input = + getClass() + .getClassLoader() + .getResourceAsStream("scripts/Authentication/db/data.sql")) { + assertNotNull(input); + String seed = new String(input.readAllBytes(), StandardCharsets.UTF_8); + String level3Row = + seed.lines() + .filter(line -> line.startsWith("INSERT INTO auth_users VALUES (3,")) + .findFirst() + .orElseThrow(); + + assertTrue(level3Row.contains("'$2a$10$")); + assertTrue(level3Row.contains("'BCRYPT'")); + assertFalse(seed.contains("Real password: 'b7X$4nRj-6mW'")); + assertFalse(level3Row.contains("'b7X$4nRj-6mW'")); + } + } + + @Test + void level1AndLevel2RoutingRemainsUnchanged() { + AuthLoginService unchangedService = mock(AuthLoginService.class); + AuthenticationVulnerability unchangedController = + new AuthenticationVulnerability(unchangedService); + AuthUser secureUser = level3User("irrelevant", AuthUserAlgorithm.BCRYPT); + when(unchangedService.authenticate(anyString(), anyString(), eq(9))) + .thenReturn(AuthLoginService.AuthResult.success(secureUser)); + + assertTrue(unchangedController.level1SQLi("user", "password").getBody().getIsValid()); + assertTrue(unchangedController.level2Logging("user", "password").getBody().getIsValid()); + verify(unchangedService, never()).authenticateLevel1SQLi(anyString(), anyString()); + verify(unchangedService, never()).authenticateLevel2Logging(anyString(), anyString()); + } + + private static AuthUser level3User(String password, AuthUserAlgorithm algorithm) { + return new AuthUser( + 3, "admin_plain", password, null, algorithm, 3, "admin_plain@example.com", "ADMIN"); + } +} diff --git a/src/test/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerabilityTest.java b/src/test/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerabilityTest.java index 17035f52c..06a5cd836 100644 --- a/src/test/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerabilityTest.java +++ b/src/test/java/org/sasanlabs/service/vulnerability/authentication/AuthenticationVulnerabilityTest.java @@ -73,7 +73,7 @@ void level2Logging_ShouldReturnProfile_WhenAuthenticated() { } @Test - void level3Plaintext_ShouldExposePasswordInResponse() { + void level3Plaintext_ShouldNotExposePasswordInResponse() { when(authLoginService.authenticate(eq("Alice"), anyString(), eq(3))) .thenReturn(AuthLoginService.AuthResult.success(ALICE)); @@ -81,8 +81,9 @@ void level3Plaintext_ShouldExposePasswordInResponse() { controller.level3Plaintext("Alice", "secret"); Map profile = (Map) response.getBody().getContent(); - // Leaks the password - assertEquals("p@ssword123", profile.get("passwordInDB")); + assertFalse(profile.containsKey("passwordInDB")); + assertFalse(profile.containsKey("passwordHash")); + assertFalse(profile.containsValue("p@ssword123")); } @Test From 5b7d88cff422b06c561692b987cde3188101af37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riki=20L=C3=A4nsilahti?= <7629042+lansiri@users.noreply.github.com> Date: Sun, 9 Aug 2026 21:56:40 +0300 Subject: [PATCH 92/92] fix(crypto): retire reversible password transforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Riki Länsilahti <7629042+lansiri@users.noreply.github.com> --- .../internal/utility/EncodingUtils.java | 6 --- .../internal/utility/EncryptionUtils.java | 44 ------------------- .../internal/utility/EncryptionUtilsTest.java | 27 ------------ .../LegacyReversibleCryptoRetirementTest.java | 25 +++++++++++ 4 files changed, 25 insertions(+), 77 deletions(-) create mode 100644 src/test/java/org/sasanlabs/internal/utility/LegacyReversibleCryptoRetirementTest.java diff --git a/src/main/java/org/sasanlabs/internal/utility/EncodingUtils.java b/src/main/java/org/sasanlabs/internal/utility/EncodingUtils.java index b6b29eb41..b7199001f 100644 --- a/src/main/java/org/sasanlabs/internal/utility/EncodingUtils.java +++ b/src/main/java/org/sasanlabs/internal/utility/EncodingUtils.java @@ -1,7 +1,5 @@ package org.sasanlabs.internal.utility; -import java.util.Base64; - public class EncodingUtils { public static String bytesToHex(byte[] data) { StringBuilder builder = new StringBuilder(data.length * 2); @@ -10,8 +8,4 @@ public static String bytesToHex(byte[] data) { } return builder.toString(); } - - public static String encodeBase64(String rawText) { - return Base64.getEncoder().encodeToString(rawText.getBytes()); - } } diff --git a/src/main/java/org/sasanlabs/internal/utility/EncryptionUtils.java b/src/main/java/org/sasanlabs/internal/utility/EncryptionUtils.java index 21caa50d1..bd43a2d1a 100644 --- a/src/main/java/org/sasanlabs/internal/utility/EncryptionUtils.java +++ b/src/main/java/org/sasanlabs/internal/utility/EncryptionUtils.java @@ -21,50 +21,6 @@ public class EncryptionUtils { private EncryptionUtils() {} - /** - * INSECURE: Caesar Cipher shifts alphabetic characters positions to the right overflowing to - * the beginning of the alphabet. 'z' will shift to 'a' and so on. - * - * @param rawPassword plaintext password to encrypt - * @param shift how many shifts right - */ - public static String caesarCipher(String rawPassword, int shift) throws EncryptionException { - - if (rawPassword == null) { - throw new EncryptionException("Raw password cannot be null "); - } - - // Technically shift can be any non-zero integer, for clarity it should be between 0-25 - // inclusive - if (shift < 0 || shift >= 26) { - throw new EncryptionException("Shift value must be between 0 and 25 inclusive."); - } - - StringBuilder builder = new StringBuilder(); - for (char ch : rawPassword.toCharArray()) { - if (Character.isLetter(ch)) { - char base = Character.isUpperCase(ch) ? 'A' : 'a'; - builder.append((char) ((ch - base + shift) % 26 + base)); - } else { - builder.append(ch); - } - } - return builder.toString(); - } - - /** - * INSECURE: Custom cipher that obscures the texts by reversing it then Base64 encodes it. - * - * @param rawPassword password to encrypt - */ - public static String customCipher(String rawPassword) throws EncryptionException { - if (rawPassword == null) { - throw new EncryptionException("Raw password cannot be null "); - } - String reversed = new StringBuilder(rawPassword).reverse().toString(); - return EncodingUtils.encodeBase64(reversed); - } - private static final byte[] salt = new byte[16]; static { diff --git a/src/test/java/org/sasanlabs/internal/utility/EncryptionUtilsTest.java b/src/test/java/org/sasanlabs/internal/utility/EncryptionUtilsTest.java index 5b81925f6..f9b15fda7 100644 --- a/src/test/java/org/sasanlabs/internal/utility/EncryptionUtilsTest.java +++ b/src/test/java/org/sasanlabs/internal/utility/EncryptionUtilsTest.java @@ -10,33 +10,6 @@ class EncryptionUtilsTest { - @Test - @DisplayName("Caesar Cipher: Should shift characters by 3 and wrap around the alphabet") - void caesarCipher_CorrectShift() throws EncryptionException { - // Basic shift - assertEquals("def", EncryptionUtils.caesarCipher("abc", 3)); - - // Wrapping shift (z -> c) - assertEquals("abc", EncryptionUtils.caesarCipher("xyz", 3)); - - // Case preservation - assertEquals("Abc", EncryptionUtils.caesarCipher("Xyz", 3)); - - // Non-alphabetic characters remain unchanged - assertEquals("123! @#", EncryptionUtils.caesarCipher("123! @#", 3)); - } - - @Test - @DisplayName( - "Custom Cipher: Should reverse the string and return a valid Base64 encoded string") - void customCipher_ReverseAndBase64() throws EncryptionException { - String input = "password"; - String reversed = "drowssap"; - String expectedBase64 = EncodingUtils.encodeBase64(reversed); - - assertEquals(expectedBase64, EncryptionUtils.customCipher(input)); - } - @Test @DisplayName("Key Generation: Should derive an AES key from a string password") void getKeyFromPassword_ValidKey() throws EncryptionException { diff --git a/src/test/java/org/sasanlabs/internal/utility/LegacyReversibleCryptoRetirementTest.java b/src/test/java/org/sasanlabs/internal/utility/LegacyReversibleCryptoRetirementTest.java new file mode 100644 index 000000000..d59d6c7e7 --- /dev/null +++ b/src/test/java/org/sasanlabs/internal/utility/LegacyReversibleCryptoRetirementTest.java @@ -0,0 +1,25 @@ +package org.sasanlabs.internal.utility; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class LegacyReversibleCryptoRetirementTest { + + @Test + void passwordUtilitiesDoNotExposeLegacyReversibleTransforms() { + Set legacyMethods = Set.of("encodeBase64", "caesarCipher", "customCipher"); + + assertFalse(hasAnyMethodNamed(EncodingUtils.class, legacyMethods)); + assertFalse(hasAnyMethodNamed(EncryptionUtils.class, legacyMethods)); + } + + private boolean hasAnyMethodNamed(Class type, Set methodNames) { + return Arrays.stream(type.getDeclaredMethods()) + .map(Method::getName) + .anyMatch(methodNames::contains); + } +}