Skip to content

Commit 2ef2e3a

Browse files
author
drighetto
committed
Add method to perform URL decoding
1 parent ab739a2 commit 2ef2e3a

File tree

2 files changed

+61
-6
lines changed

2 files changed

+61
-6
lines changed

src/main/java/eu/righettod/SecurityUtils.java

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import java.net.http.HttpClient;
4949
import java.net.http.HttpRequest;
5050
import java.net.http.HttpResponse;
51+
import java.nio.charset.Charset;
5152
import java.nio.charset.StandardCharsets;
5253
import java.nio.file.Files;
5354
import java.security.MessageDigest;
@@ -1076,11 +1077,7 @@ public static boolean isPSD2StetSafeCertificateURL(String certificateUrl) {
10761077
//- Trigger the malicious action that the attacker want but with a HTTP HEAD without any redirect and parameters.
10771078
HttpResponse<String> response;
10781079
try (HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NEVER).build()) {
1079-
HttpRequest request = HttpRequest.newBuilder()
1080-
.uri(uri)
1081-
.timeout(Duration.ofSeconds(connectionTimeoutInSeconds))
1082-
.method("HEAD", HttpRequest.BodyPublishers.noBody())
1083-
.header("User-Agent", userAgent)//To provide an hint to the target about the initiator of the request
1080+
HttpRequest request = HttpRequest.newBuilder().uri(uri).timeout(Duration.ofSeconds(connectionTimeoutInSeconds)).method("HEAD", HttpRequest.BodyPublishers.noBody()).header("User-Agent", userAgent)//To provide an hint to the target about the initiator of the request
10841081
.header("Cache-Control", "no-store, max-age=0")//To prevent caching issues or abuses
10851082
.build();
10861083
response = client.send(request, HttpResponse.BodyHandlers.ofString());
@@ -1099,5 +1096,39 @@ public static boolean isPSD2StetSafeCertificateURL(String certificateUrl) {
10991096
return isValid;
11001097
}
11011098

1102-
1099+
/**
1100+
* Perform sequential URL decoding operations against a URL encoded data until the data is not URL encoded anymore or if the specified threshold is reached.
1101+
*
1102+
* @param encodedData URL encoded data.
1103+
* @param decodingRoundThreshold Threshold above which decoding will fail.
1104+
* @return The decoded data.
1105+
* @throws SecurityException If the threshold is reached.
1106+
* @see "https://en.wikipedia.org/wiki/Percent-encoding"
1107+
* @see "https://owasp.org/www-community/Double_Encoding"
1108+
* @see "https://portswigger.net/web-security/essential-skills/obfuscating-attacks-using-encodings"
1109+
* @see "https://capec.mitre.org/data/definitions/120.html"
1110+
*/
1111+
public static String applyURLDecoding(String encodedData, int decodingRoundThreshold) throws SecurityException {
1112+
if (decodingRoundThreshold < 1) {
1113+
throw new IllegalArgumentException("Threshold must be a positive number !");
1114+
}
1115+
if (encodedData == null) {
1116+
throw new IllegalArgumentException("Data provided must not be null !");
1117+
}
1118+
Charset charset = StandardCharsets.UTF_8;
1119+
int currentDecodingRound = 0;
1120+
boolean isFinished = false;
1121+
String currentRoundData = encodedData;
1122+
String previousRoundData = encodedData;
1123+
while (!isFinished) {
1124+
if (currentDecodingRound > decodingRoundThreshold) {
1125+
throw new SecurityException(String.format("Decoding round threshold of %s reached!", decodingRoundThreshold));
1126+
}
1127+
currentRoundData = URLDecoder.decode(currentRoundData, charset);
1128+
isFinished = currentRoundData.equals(previousRoundData);
1129+
previousRoundData = currentRoundData;
1130+
currentDecodingRound++;
1131+
}
1132+
return currentRoundData;
1133+
}
11031134
}

src/test/java/eu/righettod/TestSecurityUtils.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,5 +505,29 @@ public void isPSD2StetSafeCertificateURL() {
505505
String validUrl = "https://raw.githubusercontent.com/righettod/code-snippets-security-utils/main/src/test/resources/myQsealCertificate_873dddcc49456290e2315cf3335b650715751fecdebf517e73b8642696ecc406";
506506
assertTrue(SecurityUtils.isPSD2StetSafeCertificateURL(validUrl), String.format(templateMsgIPFalsePositive, validUrl));
507507
}
508+
509+
@Test
510+
public void applyURLDecoding() {
511+
final String refDecodedData = "Hello World!!!";
512+
//Key is the decoding threshold and value is the URL encoded value (threshold times)
513+
final Map<Integer, String> testData = new HashMap<>();
514+
testData.put(1, "Hello%20World%21%21%21");
515+
testData.put(2, "Hello%2520World%2521%2521%2521");
516+
testData.put(3, "Hello%252520World%252521%252521%252521");
517+
testData.put(4, "Hello%25252520World%25252521%25252521%25252521");
518+
testData.put(5, "Hello%2525252520World%2525252521%2525252521%2525252521");
519+
testData.put(6, "Hello%252525252520World%252525252521%252525252521%252525252521");
520+
//Test valid cases
521+
testData.forEach((threshold, encodedData) -> {
522+
assertEquals(refDecodedData, SecurityUtils.applyURLDecoding(encodedData, threshold));
523+
});
524+
//Test invalid cases
525+
SecurityException thrown = assertThrows(
526+
SecurityException.class,
527+
() -> SecurityUtils.applyURLDecoding(testData.get(6), 3),
528+
"SecurityException expected!"
529+
);
530+
assertTrue(thrown.getMessage().equalsIgnoreCase("Decoding round threshold of 3 reached!"));
531+
}
508532
}
509533

0 commit comments

Comments
 (0)