From 4cc607d5ec93302f99a4619f9b8ee732b6fb7266 Mon Sep 17 00:00:00 2001 From: Irene Nunziata Date: Thu, 13 Aug 2026 22:29:58 -0400 Subject: [PATCH] Harden VulnerableApp challenge endpoints against OWASP Top 10 flaws. Apply root-cause remediations across SQLi, XSS, path traversal, command injection, SSRF, XXE, LDAP, IDOR, open redirect, clickjacking, auth, and RFI so insecure levels follow existing secure patterns. Co-authored-by: Cursor --- .../authentication/AuthLoginService.java | 35 +-- .../ClickjackingVulnerability.java | 24 +- .../commandInjection/CommandInjection.java | 33 +-- .../vulnerability/idor/IDORVulnerability.java | 13 +- .../LDAPInjectionVulnerability.java | 17 +- .../Http3xxStatusCodeBasedInjection.java | 76 +----- .../PathTraversalVulnerability.java | 79 ++---- .../vulnerability/rfi/UrlParamBasedRFI.java | 45 +--- .../BlindSQLInjectionVulnerability.java | 11 +- .../ErrorBasedSQLInjectionVulnerability.java | 229 ++++-------------- .../UnionBasedSQLInjectionVulnerability.java | 14 +- .../vulnerability/ssrf/SSRFVulnerability.java | 16 +- .../PersistentXSSInHTMLTagVulnerability.java | 54 ++--- .../xss/reflected/XSSInImgTagAttribute.java | 75 +++--- .../reflected/XSSWithHtmlTagInjection.java | 33 ++- .../vulnerability/xxe/XXEVulnerability.java | 26 +- .../authentication/AuthLoginServiceTest.java | 15 +- 17 files changed, 229 insertions(+), 566 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..655a40194 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginService.java @@ -3,12 +3,9 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.List; import java.util.Optional; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.springframework.jdbc.core.BeanPropertyRowMapper; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @@ -23,47 +20,25 @@ public class AuthLoginService { private static final Logger LOGGER = LogManager.getLogger(AuthLoginService.class); - private final JdbcTemplate jdbcTemplate; private final AuthUserRepository authUserRepository; private final BCryptPasswordEncoder passwordEncoder; public AuthLoginService( - JdbcTemplate jdbcTemplate, - AuthUserRepository authUserRepository, - BCryptPasswordEncoder passwordEncoder) { - this.jdbcTemplate = jdbcTemplate; + AuthUserRepository authUserRepository, BCryptPasswordEncoder passwordEncoder) { this.authUserRepository = authUserRepository; this.passwordEncoder = passwordEncoder; } - /** Level 1: SQL Injection. Demonstrates a login query vulnerable to string concatenation. */ + /** Level 1 authentication using the same repository-backed lookup as the secure levels. */ 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 - + "'"; - try { - // Level 1 still uses JdbcTemplate to allow SQL Injection bypass - List users = - jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(AuthUser.class)); - if (!users.isEmpty()) { - return AuthResult.success(users.get(0)); - } - } catch (Exception e) { - // In a real exploit, this might be an error-based SQLi - return AuthResult.failure("Database error: " + e.getMessage()); - } - return AuthResult.failure("Invalid credentials"); + return authenticate(username, password, 1); } - /** Level 2: Sensitive Data Logging. Logs the provided password to the logs. */ + /** Level 2: Authenticates without logging sensitive credentials. */ 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 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..97c60b936 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("Content-Security-Policy", "frame-ancestors 'none'"); + 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("Content-Security-Policy", "frame-ancestors 'none'"); 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("Content-Security-Policy", "frame-ancestors 'none'"); 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("Content-Security-Policy", "frame-ancestors 'none'"); + 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("Content-Security-Policy", "frame-ancestors 'none'"); 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..a755ad045 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,11 @@ 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); + Supplier validator = + () -> + StringUtils.isNotBlank(ipAddress) + && (IP_ADDRESS_PATTERN.matcher(ipAddress).matches() + || ipAddress.contentEquals("localhost")); return new ResponseEntity>( new GenericVulnerabilityResponseBean( this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), @@ -87,9 +91,8 @@ public ResponseEntity> getVulnerablePay Supplier validator = () -> StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find(); + && (IP_ADDRESS_PATTERN.matcher(ipAddress).matches() + || ipAddress.contentEquals("localhost")); return new ResponseEntity>( new GenericVulnerabilityResponseBean( this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), @@ -110,11 +113,8 @@ public ResponseEntity> getVulnerablePay 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"); + && (IP_ADDRESS_PATTERN.matcher(ipAddress).matches() + || ipAddress.contentEquals("localhost")); return new ResponseEntity>( new GenericVulnerabilityResponseBean( this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), @@ -136,11 +136,8 @@ public ResponseEntity> getVulnerablePay 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"); + && (IP_ADDRESS_PATTERN.matcher(ipAddress).matches() + || ipAddress.contentEquals("localhost")); return new ResponseEntity>( new GenericVulnerabilityResponseBean( this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), @@ -160,12 +157,8 @@ public ResponseEntity> getVulnerablePay 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"); + && (IP_ADDRESS_PATTERN.matcher(ipAddress).matches() + || ipAddress.contentEquals("localhost")); return new ResponseEntity>( new GenericVulnerabilityResponseBean( this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), 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..e28f4e2ae 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/idor/IDORVulnerability.java @@ -77,8 +77,11 @@ public ResponseEntity> level1( String actualToken = cookieToken; try { if (actualToken != null) { - idorLoginService.decodeToken(actualToken); + User decodedUser = idorLoginService.decodeToken(actualToken); if (id != null) { + if (decodedUser.getUserId() != id) { + return response(ACCESS_DENIED_INSUFFICIENT, false); + } User profile = fetchUserById(id); if (profile == null) { return response(USER_NOT_FOUND, false); @@ -116,8 +119,8 @@ public ResponseEntity> level2( String actualToken = cookieToken; try { if (actualToken != null && loggedInUser != null) { - idorLoginService.decodeToken(actualToken); - User profile = fetchUserById(loggedInUser); + User decodedUser = idorLoginService.decodeToken(actualToken); + User profile = fetchUserById(decodedUser.getUserId()); if (profile == null) { return response(USER_NOT_FOUND, false); } @@ -155,7 +158,7 @@ public ResponseEntity> level3( if (actualToken != null) { User decodedUser = idorLoginService.decodeToken(actualToken); int tokenUserId = decodedUser.getUserId(); - String role = cookieRole != null ? cookieRole : decodedUser.getRole(); + String role = decodedUser.getRole(); if (id == null) { id = tokenUserId; @@ -204,7 +207,7 @@ public ResponseEntity> level4( if (actualToken != null) { User decodedUser = idorLoginService.decodeToken(actualToken); int tokenUserId = decodedUser.getUserId(); - String role = cookieRole != null ? decodeBase64(cookieRole) : decodedUser.getRole(); + String role = decodedUser.getRole(); if (id == null) { id = tokenUserId; 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..908121d9a 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/ldapInjection/LDAPInjectionVulnerability.java @@ -111,8 +111,9 @@ public ResponseEntity> level1( return response("Provide username", false); } - // Vulnerable LDAP filter - String ldapQuery = "(uid=" + username + ")"; + // Encode LDAP filter values to prevent injection + String sanitizedInput = Filter.encodeValue(username); + String ldapQuery = "(uid=" + sanitizedInput + ")"; try { List users = searchUsers(ldapQuery); @@ -140,8 +141,8 @@ public ResponseEntity> level2( return response("Provide username", false); } - // 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); @@ -170,8 +171,8 @@ public ResponseEntity> level3( return response("Provide username and password", false); } - // Vulnerable authentication filter - String ldapQuery = "(&(uid=" + username + ")(uid=*))"; + String sanitizedInput = Filter.encodeValue(username); + String ldapQuery = "(&(uid=" + sanitizedInput + "))"; try { List users = searchEntries(ldapQuery); @@ -264,7 +265,9 @@ public ResponseEntity> level5( return response("Provide username and password", false); } - String ldapQuery = "(&(uid=" + username + "))"; + String sanitizedInput = Filter.encodeValue(username); + + String ldapQuery = "(&(uid=" + sanitizedInput + "))"; try { List users = searchEntries(ldapQuery); 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..a36fe00aa 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/openRedirect/Http3xxStatusCodeBasedInjection.java @@ -1,15 +1,11 @@ package org.sasanlabs.service.vulnerability.openRedirect; -import static org.sasanlabs.vulnerability.utils.Constants.NULL_BYTE_CHARACTER; - import java.net.MalformedURLException; -import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.function.Function; -import org.sasanlabs.internal.utility.FrameworkConstants; import org.sasanlabs.internal.utility.LevelConstants; import org.sasanlabs.internal.utility.Variant; import org.sasanlabs.internal.utility.annotations.AttackVector; @@ -89,7 +85,7 @@ private ResponseEntity getURLRedirectionResponseEntity( htmlTemplate = "LEVEL_1/Http3xxStatusCodeBasedInjection") public ResponseEntity getVulnerablePayloadLevel1( @RequestParam(RETURN_TO) String urlToRedirect) { - return this.getURLRedirectionResponseEntity(urlToRedirect, (url) -> true); + return this.getURLRedirectionResponseEntity(urlToRedirect, WHITELISTED_URLS::contains); } // Payloads: @@ -117,14 +113,7 @@ public ResponseEntity getVulnerablePayloadLevel1( public ResponseEntity getVulnerablePayloadLevel2( RequestEntity requestEntity, @RequestParam(RETURN_TO) String urlToRedirect) throws MalformedURLException { - URL requestUrl = new URL(requestEntity.getUrl().toString()); - return this.getURLRedirectionResponseEntity( - urlToRedirect, - (url) -> - (!url.startsWith(FrameworkConstants.HTTP) - && !url.startsWith(FrameworkConstants.HTTPS) - && !url.startsWith(FrameworkConstants.WWW)) - || requestUrl.getAuthority().equals(urlToRedirect)); + return this.getURLRedirectionResponseEntity(urlToRedirect, WHITELISTED_URLS::contains); } // Payloads: @@ -151,15 +140,7 @@ public ResponseEntity getVulnerablePayloadLevel2( public ResponseEntity getVulnerablePayloadLevel3( RequestEntity requestEntity, @RequestParam(RETURN_TO) String urlToRedirect) throws MalformedURLException { - URL requestUrl = new URL(requestEntity.getUrl().toString()); - return this.getURLRedirectionResponseEntity( - urlToRedirect, - (url) -> - (!url.startsWith(FrameworkConstants.HTTP) - && !url.startsWith(FrameworkConstants.HTTPS) - && !url.startsWith("//") - && !url.startsWith(FrameworkConstants.WWW)) - || requestUrl.getAuthority().equals(url)); + return this.getURLRedirectionResponseEntity(urlToRedirect, WHITELISTED_URLS::contains); } // As there can be too many hacks e.g. using %00 to %1F so blacklisting is not possible @@ -184,16 +165,7 @@ public ResponseEntity getVulnerablePayloadLevel3( public ResponseEntity getVulnerablePayloadLevel4( RequestEntity requestEntity, @RequestParam(RETURN_TO) String urlToRedirect) throws MalformedURLException { - URL requestUrl = new URL(requestEntity.getUrl().toString()); - return this.getURLRedirectionResponseEntity( - urlToRedirect, - (url) -> - (!url.startsWith(FrameworkConstants.HTTP) - && !url.startsWith(FrameworkConstants.HTTPS) - && !url.startsWith(FrameworkConstants.WWW) - && !url.startsWith("//") - && !url.startsWith(NULL_BYTE_CHARACTER)) - || requestUrl.getAuthority().equals(url)); + return this.getURLRedirectionResponseEntity(urlToRedirect, WHITELISTED_URLS::contains); } // Payloads: @@ -221,17 +193,7 @@ public ResponseEntity getVulnerablePayloadLevel4( public ResponseEntity getVulnerablePayloadLevel5( RequestEntity requestEntity, @RequestParam(RETURN_TO) String urlToRedirect) throws MalformedURLException { - URL requestUrl = new URL(requestEntity.getUrl().toString()); - return this.getURLRedirectionResponseEntity( - urlToRedirect, - (url) -> - (!url.startsWith(FrameworkConstants.HTTP) - && !url.startsWith(FrameworkConstants.HTTPS) - && !url.startsWith("//") - && !url.startsWith(FrameworkConstants.WWW) - && !url.startsWith(NULL_BYTE_CHARACTER) - && (url.length() > 0 && url.charAt(0) > 20)) - || requestUrl.getAuthority().equals(url)); + return this.getURLRedirectionResponseEntity(urlToRedirect, WHITELISTED_URLS::contains); } // case study explaning issue with this approach: @@ -257,13 +219,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 this.getURLRedirectionResponseEntity(urlToRedirect, WHITELISTED_URLS::contains); } @AttackVector( @@ -287,21 +243,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 this.getURLRedirectionResponseEntity(urlToRedirect, WHITELISTED_URLS::contains); } // using whitelisting approach @@ -335,7 +277,7 @@ public ResponseEntity getVulnerablePayloadLevel8( htmlTemplate = "LEVEL_9/Http3xxStatusCodeBasedInjection") public ResponseEntity getVulnerablePayloadLevel9( @RequestParam(RETURN_TO) String urlToRedirect) { - return this.getURLRedirectionResponseEntity(urlToRedirect, (url) -> true); + return this.getURLRedirectionResponseEntity(urlToRedirect, WHITELISTED_URLS::contains); } // Payloads: any URL e.g. /VulnerableApp/phishing/fake-login.html @@ -360,7 +302,7 @@ public ResponseEntity getVulnerablePayloadLevel9( htmlTemplate = "LEVEL_10/Http3xxStatusCodeBasedInjection") public ResponseEntity getVulnerablePayloadLevel10( @RequestParam(RETURN_TO) String urlToRedirect) { - return this.getURLRedirectionResponseEntity(urlToRedirect, (url) -> true); + return this.getURLRedirectionResponseEntity(urlToRedirect, WHITELISTED_URLS::contains); } @AttackVector( 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..13de3d02a 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/pathTraversal/PathTraversalVulnerability.java @@ -79,7 +79,8 @@ private ResponseEntity> readFile( public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam Map queryParams) { String fileName = queryParams.get(URL_PARAM_KEY); - return this.readFile(() -> fileName != null, fileName); + return this.readFile( + () -> fileName != null && ALLOWED_FILE_NAMES.contains(fileName), fileName); } @AttackVector( @@ -92,8 +93,7 @@ public ResponseEntity> getVulnerablePay RequestEntity requestEntity, @RequestParam Map queryParams) { String fileName = queryParams.get(URL_PARAM_KEY); return this.readFile( - () -> !requestEntity.getUrl().toString().contains("../") && fileName != null, - fileName); + () -> fileName != null && ALLOWED_FILE_NAMES.contains(fileName), fileName); } @AttackVector( @@ -106,8 +106,7 @@ public ResponseEntity> getVulnerablePay RequestEntity requestEntity, @RequestParam Map queryParams) { String fileName = queryParams.get(URL_PARAM_KEY); return this.readFile( - () -> !requestEntity.getUrl().toString().contains("..") && fileName != null, - fileName); + () -> fileName != null && ALLOWED_FILE_NAMES.contains(fileName), fileName); } @AttackVector( @@ -121,11 +120,7 @@ public ResponseEntity> getVulnerablePay RequestEntity requestEntity, @RequestParam Map queryParams) { String fileName = queryParams.get(URL_PARAM_KEY); return this.readFile( - () -> - !requestEntity.getUrl().toString().contains("..") - && !requestEntity.getUrl().toString().contains("%2f") - && fileName != null, - fileName); + () -> fileName != null && ALLOWED_FILE_NAMES.contains(fileName), fileName); } @AttackVector( @@ -139,11 +134,7 @@ public ResponseEntity> getVulnerablePay RequestEntity requestEntity, @RequestParam Map queryParams) { String fileName = queryParams.get(URL_PARAM_KEY); return this.readFile( - () -> - !requestEntity.getUrl().toString().contains("..") - && !requestEntity.getUrl().toString().toLowerCase().contains("%2f") - && fileName != null, - fileName); + () -> fileName != null && ALLOWED_FILE_NAMES.contains(fileName), fileName); } @AttackVector( @@ -156,7 +147,8 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel6( @RequestParam Map queryParams) { String fileName = queryParams.get(URL_PARAM_KEY); - return this.readFile(() -> fileName != null && !fileName.contains(".."), fileName); + return this.readFile( + () -> fileName != null && ALLOWED_FILE_NAMES.contains(fileName), fileName); } // Null Byte @@ -179,13 +171,7 @@ public ResponseEntity> getVulnerablePay : queryFileName; } return this.readFile( - () -> - queryFileName != null - && ALLOWED_FILE_NAMES.stream() - .anyMatch( - allowedFileName -> - queryFileName.contains(allowedFileName)), - fileName); + () -> fileName != null && ALLOWED_FILE_NAMES.contains(fileName), fileName); } @AttackVector( @@ -207,14 +193,7 @@ public ResponseEntity> getVulnerablePay : queryFileName; } return this.readFile( - () -> - queryFileName != null - && !requestEntity.getUrl().toString().contains("../") - && ALLOWED_FILE_NAMES.stream() - .anyMatch( - allowedFileName -> - queryFileName.contains(allowedFileName)), - fileName); + () -> fileName != null && ALLOWED_FILE_NAMES.contains(fileName), fileName); } @AttackVector( @@ -236,14 +215,7 @@ public ResponseEntity> getVulnerablePay : queryFileName; } return this.readFile( - () -> - queryFileName != null - && !requestEntity.getUrl().toString().contains("..") - && ALLOWED_FILE_NAMES.stream() - .anyMatch( - allowedFileName -> - queryFileName.contains(allowedFileName)), - fileName); + () -> fileName != null && ALLOWED_FILE_NAMES.contains(fileName), fileName); } @AttackVector( @@ -265,15 +237,7 @@ public ResponseEntity> getVulnerablePay : queryFileName; } return this.readFile( - () -> - queryFileName != null - && !requestEntity.getUrl().toString().contains("..") - && !requestEntity.getUrl().toString().contains("%2f") - && ALLOWED_FILE_NAMES.stream() - .anyMatch( - allowedFileName -> - queryFileName.contains(allowedFileName)), - fileName); + () -> fileName != null && ALLOWED_FILE_NAMES.contains(fileName), fileName); } @AttackVector( @@ -295,15 +259,7 @@ public ResponseEntity> getVulnerablePay : queryFileName; } return this.readFile( - () -> - queryFileName != null - && !requestEntity.getUrl().toString().contains("..") - && !requestEntity.getUrl().toString().toLowerCase().contains("%2f") - && ALLOWED_FILE_NAMES.stream() - .anyMatch( - allowedFileName -> - queryFileName.contains(allowedFileName)), - fileName); + () -> fileName != null && ALLOWED_FILE_NAMES.contains(fileName), fileName); } @AttackVector( @@ -325,13 +281,6 @@ public ResponseEntity> getVulnerablePay : queryFileName; } return this.readFile( - () -> - queryFileName != null - && !queryFileName.contains("..") - && ALLOWED_FILE_NAMES.stream() - .anyMatch( - allowedFileName -> - queryFileName.contains(allowedFileName)), - fileName); + () -> fileName != null && ALLOWED_FILE_NAMES.contains(fileName), fileName); } } 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..2e9720601 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/rfi/UrlParamBasedRFI.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/rfi/UrlParamBasedRFI.java @@ -1,23 +1,14 @@ 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; import java.util.Map; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.sasanlabs.internal.utility.GenericUtils; import org.sasanlabs.internal.utility.LevelConstants; import org.sasanlabs.internal.utility.annotations.VulnerableAppRequestMapping; import org.sasanlabs.internal.utility.annotations.VulnerableAppRestController; -import org.sasanlabs.service.vulnerability.pathTraversal.PathTraversalVulnerability; import org.springframework.context.annotation.Profile; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.client.RestTemplate; /** * @author KSASAN preetkaran20@gmail.com @@ -28,48 +19,18 @@ value = "RemoteFileInclusion") public class UrlParamBasedRFI { - private static final transient Logger LOGGER = - LogManager.getLogger(PathTraversalVulnerability.class); - - private static final String URL_PARAM_KEY = "url"; - @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) { - try { - URL url = new URL(queryParameterURL); - RestTemplate restTemplate = new RestTemplate(); - payload.append(restTemplate.getForObject(url.toURI(), String.class)); - } catch (IOException | URISyntaxException e) { - LOGGER.error("Following error occurred:", e); - } - } - + // Do not fetch arbitrary remote URLs; return an empty wrapped payload. return new ResponseEntity<>( - GenericUtils.wrapPayloadInGenericVulnerableAppTemplate(payload.toString()), - HttpStatus.OK); + GenericUtils.wrapPayloadInGenericVulnerableAppTemplate(""), HttpStatus.OK); } @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2) 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)) { - try { - URL url = new URL(queryParameterURL); - RestTemplate restTemplate = new RestTemplate(); - payload.append(restTemplate.getForObject(url.toURI(), String.class)); - } catch (IOException | URISyntaxException e) { - LOGGER.error("Following error occurred:", e); - } - } - return new ResponseEntity<>( - GenericUtils.wrapPayloadInGenericVulnerableAppTemplate(payload.toString()), - HttpStatus.OK); + GenericUtils.wrapPayloadInGenericVulnerableAppTemplate(""), HttpStatus.OK); } } 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..dc04249d9 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/BlindSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/BlindSQLInjectionVulnerability.java @@ -89,8 +89,12 @@ public ResponseEntity getCarInformationLevel1( @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, + (conn) -> conn.prepareStatement("select * from cars where id=?"), + (prepareStatement) -> { + prepareStatement.setString(1, id); + }, (rs) -> { if (rs.next()) { return bodyBuilder.body(CAR_IS_PRESENT_RESPONSE); @@ -132,7 +136,10 @@ public ResponseEntity getCarInformationLevel2( BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); bodyBuilder.body(ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); return applicationJdbcTemplate.query( - "select * from cars where id='" + id + "'", + (conn) -> conn.prepareStatement("select * from cars where id=?"), + (prepareStatement) -> { + prepareStatement.setString(1, id); + }, (rs) -> { if (rs.next()) { return bodyBuilder.body(CAR_IS_PRESENT_RESPONSE); 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..0bb962381 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/ErrorBasedSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/ErrorBasedSQLInjectionVulnerability.java @@ -50,6 +50,43 @@ public ErrorBasedSQLInjectionVulnerability( this.applicationJdbcTemplate = applicationJdbcTemplate; } + private ResponseEntity queryCarByIdSecure(String id) { + BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); + bodyBuilder.body(ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); + try { + return applicationJdbcTemplate.query( + (conn) -> conn.prepareStatement("select * from cars where id=?"), + (prepareStatement) -> { + prepareStatement.setString(1, 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( + ErrorBasedSQLInjectionVulnerability + .CAR_IS_NOT_PRESENT_RESPONSE); + } + } else { + return bodyBuilder.body( + ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); + } + }); + } catch (Exception ex) { + LOGGER.error("Following error occurred", ex); + return bodyBuilder.body( + ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); + } + } + @AttackVector( vulnerabilityExposed = VulnerabilityType.ERROR_BASED_SQL_INJECTION, description = "ERROR_SQL_INJECTION_URL_PARAM_APPENDED_DIRECTLY_TO_QUERY", @@ -59,39 +96,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 queryCarByIdSecure(queryParams.get(Constants.ID)); } @AttackVector( @@ -104,42 +109,9 @@ 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 queryCarByIdSecure(queryParams.get(Constants.ID)); } - // https://stackoverflow.com/questions/15537368/how-can-sanitation-that-escapes-single-quotes-be-defeated-by-sql-injection-in-sq @AttackVector( vulnerabilityExposed = VulnerabilityType.ERROR_BASED_SQL_INJECTION, description = @@ -150,47 +122,9 @@ 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 queryCarByIdSecure(queryParams.get(Constants.ID)); } - // Assumption that only creating PreparedStatement object can save is wrong. You - // need to use the parameterized query properly. @AttackVector( vulnerabilityExposed = VulnerabilityType.ERROR_BASED_SQL_INJECTION, description = "ERROR_SQL_INJECTION_URL_PARAM_APPENDED_TO_PARAMETERIZED_QUERY", @@ -200,45 +134,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 queryCarByIdSecure(queryParams.get(Constants.ID)); } @VulnerableAppRequestMapping( @@ -247,47 +143,6 @@ public ResponseEntity doesCarInformationExistsLevel4( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity doesCarInformationExistsLevel5( @RequestParam Map queryParams) { - final String id = queryParams.get(Constants.ID); - BodyBuilder bodyBuilder = ResponseEntity.status(HttpStatus.OK); - bodyBuilder.body(ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); - try { - ResponseEntity responseEntity = - applicationJdbcTemplate.query( - (conn) -> conn.prepareStatement("select * from cars where id=?"), - (prepareStatement) -> { - prepareStatement.setString(1, id); - }, - (rs) -> { - CarInformation carInformation = new CarInformation(); - if (rs.next()) { - 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( - ErrorBasedSQLInjectionVulnerability - .CAR_IS_NOT_PRESENT_RESPONSE); - } - } else { - return bodyBuilder.body( - ErrorBasedSQLInjectionVulnerability - .CAR_IS_NOT_PRESENT_RESPONSE); - } - }); - - return responseEntity; - - } catch (Exception ex) { - LOGGER.error("Following error occurred", ex); - return bodyBuilder.body( - ErrorBasedSQLInjectionVulnerability.CAR_IS_NOT_PRESENT_RESPONSE); - } + return queryCarByIdSecure(queryParams.get(Constants.ID)); } } 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..9faea88e8 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/sqlInjection/UnionBasedSQLInjectionVulnerability.java @@ -66,7 +66,9 @@ public ResponseEntity getCarInformationLevel1( @RequestParam final Map queryParams) { final String id = queryParams.get("id"); return applicationJdbcTemplate.query( - "select * from cars where id=" + id, this::resultSetToResponse); + "select * from cars where id=?", + prepareStatement -> prepareStatement.setString(1, id), + this::resultSetToResponse); } @AttackVector( @@ -81,7 +83,9 @@ public ResponseEntity getCarInformationLevel2( @RequestParam final Map queryParams) { final String id = queryParams.get("id"); return applicationJdbcTemplate.query( - "select * from cars where id='" + id + "'", this::resultSetToResponse); + "select * from cars where id=?", + prepareStatement -> prepareStatement.setString(1, id), + this::resultSetToResponse); } @AttackVector( @@ -94,9 +98,11 @@ public ResponseEntity getCarInformationLevel2( htmlTemplate = "LEVEL_1/SQLInjection_Level1") public ResponseEntity getCarInformationLevel3( @RequestParam final Map queryParams) { - final String id = queryParams.get("id").replaceAll("'", ""); + final String id = queryParams.get("id"); return applicationJdbcTemplate.query( - "select * from cars where id='" + id + "'", this::resultSetToResponse); + "select * from cars where id=?", + prepareStatement -> prepareStatement.setString(1, id), + this::resultSetToResponse); } @VulnerableAppRequestMapping( 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..97e5f62e7 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/ssrf/SSRFVulnerability.java @@ -95,7 +95,7 @@ String getResponseForURLConnection(URL u) throws IOException { @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/SSRF") public ResponseEntity> getVulnerablePayloadLevel1( @RequestParam(FILE_URL) String url) throws IOException { - if (isUrlValid(url)) { + if (gistUrl.equalsIgnoreCase(url)) { return getGenericVulnerabilityResponseWhenURL(url); } else { return invalidUrlResponse(); @@ -109,10 +109,8 @@ public ResponseEntity> getVulnerablePay @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/SSRF") public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam(FILE_URL) String url) throws IOException { - if (isUrlValid(url) && !url.startsWith(FILE_PROTOCOL)) { - + if (gistUrl.equalsIgnoreCase(url)) { return getGenericVulnerabilityResponseWhenURL(url); - } else { return invalidUrlResponse(); } @@ -125,10 +123,7 @@ public ResponseEntity> getVulnerablePay @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/SSRF") public ResponseEntity> getVulnerablePayloadLevel3( @RequestParam(FILE_URL) String url) throws IOException { - if (isUrlValid(url) && !url.startsWith(FILE_PROTOCOL)) { - if (new URL(url).getHost().equals("169.254.169.254")) { - return this.invalidUrlResponse(); - } + if (gistUrl.equalsIgnoreCase(url)) { return getGenericVulnerabilityResponseWhenURL(url); } else { return this.invalidUrlResponse(); @@ -142,10 +137,7 @@ public ResponseEntity> getVulnerablePay @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_4, htmlTemplate = "LEVEL_1/SSRF") public ResponseEntity> getVulnerablePayloadLevel4( @RequestParam(FILE_URL) String url) throws IOException { - if (isUrlValid(url) && !url.startsWith(FILE_PROTOCOL)) { - if (MetaDataServiceMock.isPresent(new URL(url))) { - return this.invalidUrlResponse(); - } + if (gistUrl.equalsIgnoreCase(url)) { return getGenericVulnerabilityResponseWhenURL(url); } else { return this.invalidUrlResponse(); 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..4addbfe01 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 @@ -97,7 +97,10 @@ private boolean nullByteVulnerablePatternChecker(String post, Pattern pattern) { public ResponseEntity getVulnerablePayloadLevel1( @RequestParam Map queryParams) { return new ResponseEntity( - this.getCommentsPayload(queryParams, LevelConstants.LEVEL_1, post -> post), + this.getCommentsPayload( + queryParams, + LevelConstants.LEVEL_1, + post -> StringEscapeUtils.escapeHtml4(post)), HttpStatus.OK); } @@ -114,7 +117,7 @@ public ResponseEntity getVulnerablePayloadLevel2( this.getCommentsPayload( queryParams, LevelConstants.LEVEL_2, - post -> IMG_INPUT_TAG_PATTERN.matcher(post).replaceAll("")), + post -> StringEscapeUtils.escapeHtml4(post)), HttpStatus.OK); } @@ -132,10 +135,7 @@ public ResponseEntity getVulnerablePayloadLevel3( this.getCommentsPayload( queryParams, LevelConstants.LEVEL_3, - post -> - IMG_INPUT_TAG_CASE_INSENSITIVE_PATTERN - .matcher(post) - .replaceAll("")), + post -> StringEscapeUtils.escapeHtml4(post)), HttpStatus.OK); } @@ -149,16 +149,11 @@ public ResponseEntity getVulnerablePayloadLevel3( htmlTemplate = "LEVEL_1/PersistentXSS") public ResponseEntity getVulnerablePayloadLevel4( @RequestParam Map queryParams) { - Function function = - (post) -> { - boolean containsHarmfulTags = - this.nullByteVulnerablePatternChecker(post, IMG_INPUT_TAG_PATTERN); - return containsHarmfulTags - ? IMG_INPUT_TAG_PATTERN.matcher(post).replaceAll("") - : post; - }; return new ResponseEntity( - this.getCommentsPayload(queryParams, LevelConstants.LEVEL_4, function), + this.getCommentsPayload( + queryParams, + LevelConstants.LEVEL_4, + post -> StringEscapeUtils.escapeHtml4(post)), HttpStatus.OK); } @@ -171,17 +166,11 @@ public ResponseEntity getVulnerablePayloadLevel4( htmlTemplate = "LEVEL_1/PersistentXSS") public ResponseEntity getVulnerablePayloadLevel5( @RequestParam Map queryParams) { - Function function = - (post) -> { - boolean containsHarmfulTags = - this.nullByteVulnerablePatternChecker( - post, IMG_INPUT_TAG_CASE_INSENSITIVE_PATTERN); - return containsHarmfulTags - ? IMG_INPUT_TAG_CASE_INSENSITIVE_PATTERN.matcher(post).replaceAll("") - : post; - }; return new ResponseEntity( - this.getCommentsPayload(queryParams, LevelConstants.LEVEL_5, function), + this.getCommentsPayload( + queryParams, + LevelConstants.LEVEL_5, + post -> StringEscapeUtils.escapeHtml4(post)), HttpStatus.OK); } @@ -194,18 +183,11 @@ public ResponseEntity getVulnerablePayloadLevel5( htmlTemplate = "LEVEL_1/PersistentXSS") public ResponseEntity getVulnerablePayloadLevel6( @RequestParam Map queryParams) { - Function function = - (post) -> { - // This logic represents null byte vulnerable escapeHtml function - return post.contains(Constants.NULL_BYTE_CHARACTER) - ? StringEscapeUtils.escapeHtml4( - post.substring( - 0, post.indexOf(Constants.NULL_BYTE_CHARACTER))) - + post.substring(post.indexOf(Constants.NULL_BYTE_CHARACTER)) - : StringEscapeUtils.escapeHtml4(post); - }; return new ResponseEntity( - this.getCommentsPayload(queryParams, LevelConstants.LEVEL_6, function), + this.getCommentsPayload( + queryParams, + LevelConstants.LEVEL_6, + post -> StringEscapeUtils.escapeHtml4(post)), HttpStatus.OK); } 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..1dcb1568f 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; @@ -50,10 +49,16 @@ public XSSInImgTagAttribute() { public ResponseEntity getVulnerablePayloadLevel1( @RequestParam(PARAMETER_NAME) String imageLocation) { - String vulnerablePayloadWithPlaceHolder = ""; + String vulnerablePayloadWithPlaceHolder = ""; - return new ResponseEntity<>( - String.format(vulnerablePayloadWithPlaceHolder, imageLocation), HttpStatus.OK); + if (allowedValues.contains(imageLocation)) { + return new ResponseEntity<>( + String.format( + vulnerablePayloadWithPlaceHolder, + StringEscapeUtils.escapeHtml4(imageLocation)), + HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } // Adding Untrusted Data into Src tag between quotes is beneficial but not @@ -67,9 +72,14 @@ public ResponseEntity getVulnerablePayloadLevel2( String vulnerablePayloadWithPlaceHolder = ""; - String payload = String.format(vulnerablePayloadWithPlaceHolder, imageLocation); - - return new ResponseEntity<>(payload, HttpStatus.OK); + if (allowedValues.contains(imageLocation)) { + String payload = + String.format( + vulnerablePayloadWithPlaceHolder, + StringEscapeUtils.escapeHtml4(imageLocation)); + return new ResponseEntity<>(payload, HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } // Good way for HTML escapes so hacker cannot close the tags but can use event @@ -81,14 +91,16 @@ public ResponseEntity getVulnerablePayloadLevel2( public ResponseEntity getVulnerablePayloadLevel3( @RequestParam(PARAMETER_NAME) String imageLocation) { - String vulnerablePayloadWithPlaceHolder = ""; - - String payload = - String.format( - vulnerablePayloadWithPlaceHolder, - StringEscapeUtils.escapeHtml4(imageLocation)); + String vulnerablePayloadWithPlaceHolder = ""; - return new ResponseEntity<>(payload, HttpStatus.OK); + if (allowedValues.contains(imageLocation)) { + String payload = + String.format( + vulnerablePayloadWithPlaceHolder, + StringEscapeUtils.escapeHtml4(imageLocation)); + return new ResponseEntity<>(payload, HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } // Good way for HTML escapes so hacker cannot close the tags and also cannot pass brackets but @@ -102,17 +114,16 @@ public ResponseEntity getVulnerablePayloadLevel3( public ResponseEntity getVulnerablePayloadLevel4( @RequestParam(PARAMETER_NAME) String imageLocation) { - String vulnerablePayloadWithPlaceHolder = ""; - StringBuilder payload = new StringBuilder(); + String vulnerablePayloadWithPlaceHolder = ""; - if (!imageLocation.contains("(") || !imageLocation.contains(")")) { - payload.append( + if (allowedValues.contains(imageLocation)) { + return new ResponseEntity<>( String.format( vulnerablePayloadWithPlaceHolder, - StringEscapeUtils.escapeHtml4(imageLocation))); + StringEscapeUtils.escapeHtml4(imageLocation)), + HttpStatus.OK); } - - return new ResponseEntity<>(payload.toString(), HttpStatus.OK); + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } // Assume here that there is a validator vulnerable to Null Byte which validates the file name @@ -125,26 +136,16 @@ public ResponseEntity getVulnerablePayloadLevel4( 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)); - } + String vulnerablePayloadWithPlaceHolder = ""; - if (allowedValues.contains(validatedFileName)) { - payload.append( + if (allowedValues.contains(imageLocation)) { + return new ResponseEntity<>( String.format( vulnerablePayloadWithPlaceHolder, - StringEscapeUtils.escapeHtml4(imageLocation))); + StringEscapeUtils.escapeHtml4(imageLocation)), + HttpStatus.OK); } - - return new ResponseEntity<>(payload.toString(), HttpStatus.OK); + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } // 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..3d24ffcc4 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,10 +33,13 @@ public class XSSWithHtmlTagInjection { @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_1, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel1( @RequestParam Map queryParams) { - String vulnerablePayloadWithPlaceHolder = "
%s
"; + String vulnerablePayloadWithPlaceHolder = "
%s
"; StringBuilder payload = new StringBuilder(); for (Map.Entry map : queryParams.entrySet()) { - payload.append(String.format(vulnerablePayloadWithPlaceHolder, map.getValue())); + payload.append( + String.format( + vulnerablePayloadWithPlaceHolder, + StringEscapeUtils.escapeHtml4(map.getValue()))); } return new ResponseEntity(payload.toString(), HttpStatus.OK); } @@ -54,14 +55,13 @@ public ResponseEntity getVulnerablePayloadLevel1( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_2, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel2( @RequestParam Map queryParams) { - String vulnerablePayloadWithPlaceHolder = "
%s
"; + 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())); - } + payload.append( + String.format( + vulnerablePayloadWithPlaceHolder, + StringEscapeUtils.escapeHtml4(map.getValue()))); } return new ResponseEntity(payload.toString(), HttpStatus.OK); } @@ -77,16 +77,13 @@ public ResponseEntity getVulnerablePayloadLevel2( @VulnerableAppRequestMapping(value = LevelConstants.LEVEL_3, htmlTemplate = "LEVEL_1/XSS") public ResponseEntity getVulnerablePayloadLevel3( @RequestParam Map queryParams) { - String vulnerablePayloadWithPlaceHolder = "
%s
"; + 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())); - } + payload.append( + String.format( + vulnerablePayloadWithPlaceHolder, + StringEscapeUtils.escapeHtml4(map.getValue()))); } return new ResponseEntity(payload.toString(), HttpStatus.OK); } 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..345ff40ba 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/xxe/XXEVulnerability.java @@ -55,12 +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"); this.bookEntityRepository = bookEntityRepository; } - // No XXE protection + // Secure XXE parsing: disallow DOCTYPE and external entities @AttackVector(vulnerabilityExposed = VulnerabilityType.XXE, description = "XXE_NO_VALIDATION") @VulnerableAppRequestMapping( value = LevelConstants.LEVEL_1, @@ -70,17 +68,12 @@ public ResponseEntity> getVulnerablePaylo 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); + SAXParserFactory spf = SAXParserFactory.newInstance(); + spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + spf.setFeature("http://xml.org/sax/features/external-general-entities", false); + spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + return saveJaxBBasedBookInformation(spf, in, LevelConstants.LEVEL_1); } catch (Exception e) { LOGGER.error(e); } @@ -145,9 +138,12 @@ public ResponseEntity> getVulnerablePaylo HttpServletRequest request) { try { InputStream in = request.getInputStream(); - // Only disabling external Entities + // Disabling DocType and external entities SAXParserFactory spf = SAXParserFactory.newInstance(); + spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); spf.setFeature("http://xml.org/sax/features/external-general-entities", false); + spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); return saveJaxBBasedBookInformation(spf, in, LevelConstants.LEVEL_2); } catch (Exception e) { LOGGER.error(e); diff --git a/src/test/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginServiceTest.java b/src/test/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginServiceTest.java index 21b3aeeb7..a796faeaa 100644 --- a/src/test/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginServiceTest.java +++ b/src/test/java/org/sasanlabs/service/vulnerability/authentication/AuthLoginServiceTest.java @@ -3,37 +3,29 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; -import java.util.Arrays; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; class AuthLoginServiceTest { - private JdbcTemplate jdbcTemplate; private AuthUserRepository authUserRepository; private AuthLoginService authLoginService; @BeforeEach void setup() { - jdbcTemplate = mock(JdbcTemplate.class); authUserRepository = mock(AuthUserRepository.class); - authLoginService = - new AuthLoginService(jdbcTemplate, authUserRepository, new BCryptPasswordEncoder()); + authLoginService = new AuthLoginService(authUserRepository, new BCryptPasswordEncoder()); } // ======================================================================== - // Level 1 — SQL Injection (Still uses JdbcTemplate) + // Level 1 — Repository-backed authentication // ======================================================================== @Test @@ -48,7 +40,8 @@ void authenticateLevel1SQLi_ShouldReturnUser_WhenCredentialsMatch() { 1, "a@e.com", "ADMIN"); - when(jdbcTemplate.query(anyString(), any(RowMapper.class))).thenReturn(Arrays.asList(user)); + when(authUserRepository.findByUsernameAndLevel("admin_sqli", 1)) + .thenReturn(Optional.of(user)); AuthLoginService.AuthResult result = authLoginService.authenticateLevel1SQLi("admin_sqli", "pw");