Skip to content

Commit a252c5b

Browse files
committed
ascanrules: Address External Redirect False Positives
Signed-off-by: kingthorin <[email protected]>
1 parent 1f19fbe commit a252c5b

File tree

3 files changed

+108
-1
lines changed

3 files changed

+108
-1
lines changed

addOns/ascanrules/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1919
- For Alerts raised by the SQL Injection scan rules the Attack field values are now simply the payload, not an assembled description.
2020
- The Cross Site Scripting (Reflected) scan rule was updated to address potential false negatives when the injection context is a tag name and there is some filtering.
2121
- The Path Traversal scan rule now includes further details when directory matches are made (Issue 8379).
22+
- The External Redirect scan rules has been updated to account for potential false positives involving JavaScript comments.
2223

2324
### Added
2425
- Rules (as applicable) have been tagged in relation to HIPAA and PCI DSS.

addOns/ascanrules/src/main/java/org/zaproxy/zap/extension/ascanrules/ExternalRedirectScanRule.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
import java.util.ArrayList;
2424
import java.util.Collections;
2525
import java.util.HashMap;
26+
import java.util.HashSet;
2627
import java.util.List;
2728
import java.util.Map;
29+
import java.util.Set;
2830
import java.util.UUID;
2931
import java.util.regex.Matcher;
3032
import java.util.regex.Pattern;
@@ -489,11 +491,74 @@ private static RedirectType isRedirected(String payload, HttpMessage msg) {
489491
}
490492

491493
private static boolean isRedirectPresent(Pattern pattern, String value) {
492-
Matcher matcher = pattern.matcher(value);
494+
Set<String> extractedComments = extractJsComments(value);
495+
String[] valueWithoutComments = {value};
496+
extractedComments.forEach(
497+
comment -> valueWithoutComments[0] = valueWithoutComments[0].replace(comment, ""));
498+
499+
Matcher matcher = pattern.matcher(valueWithoutComments[0]);
500+
493501
return matcher.find()
494502
&& StringUtils.startsWithIgnoreCase(matcher.group(1), HttpHeader.HTTP);
495503
}
496504

505+
private static Set<String> extractJsComments(String jsCode) {
506+
Set<String> comments = new HashSet<>();
507+
boolean inSingleQuote = false;
508+
boolean inDoubleQuote = false;
509+
boolean inEscape = false;
510+
511+
for (int i = 0; i < jsCode.length(); i++) {
512+
char c = jsCode.charAt(i);
513+
514+
if (inEscape) {
515+
inEscape = false;
516+
continue;
517+
}
518+
519+
if (c == '\\') {
520+
inEscape = true;
521+
continue;
522+
}
523+
524+
if (c == '\'' && !inDoubleQuote) {
525+
inSingleQuote = !inSingleQuote;
526+
continue;
527+
}
528+
529+
if (c == '"' && !inSingleQuote) {
530+
inDoubleQuote = !inDoubleQuote;
531+
continue;
532+
}
533+
534+
if (!inSingleQuote && !inDoubleQuote) {
535+
// Safe to check for comments
536+
if (c == '/' && i + 1 < jsCode.length()) {
537+
if (jsCode.charAt(i + 1) == '/') {
538+
// Single-line comment
539+
int end = jsCode.indexOf('\n', i + 2);
540+
if (end == -1) {
541+
end = jsCode.length();
542+
}
543+
comments.add(jsCode.substring(i, end));
544+
i = end - 1;
545+
continue;
546+
} else if (jsCode.charAt(i + 1) == '*') {
547+
// Multi-line comment
548+
int end = jsCode.indexOf("*/", i + 2);
549+
if (end == -1) {
550+
end = jsCode.length() - 2;
551+
}
552+
comments.add(jsCode.substring(i, end + 2));
553+
i = end + 1;
554+
continue;
555+
}
556+
}
557+
}
558+
}
559+
return comments;
560+
}
561+
497562
/**
498563
* Give back the risk associated to this vulnerability (high)
499564
*

addOns/ascanrules/src/test/java/org/zaproxy/zap/extension/ascanrules/ExternalRedirectScanRuleUnitTest.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,47 @@ void shouldReportRedirectWithJsLocationMethods(String jsMethod) throws Exception
531531
assertThat(alertsRaised.get(0).getEvidence().startsWith(HttpHeader.HTTP), equalTo(true));
532532
}
533533

534+
@Test
535+
void shouldNotReportRedirectIfInsideJsComment() throws Exception {
536+
// Given
537+
String test = "/";
538+
String body =
539+
"""
540+
<!DOCTYPE html>
541+
<html>
542+
<head>
543+
<title>Redirect commented out</title>
544+
</head>
545+
<body>
546+
547+
<script>function myRedirectFunction()
548+
{/*
549+
window.location.replace('@@@content@@@');
550+
*/}
551+
//myRedirectFunction();</script>
552+
""";
553+
nano.addHandler(
554+
new NanoServerHandler(test) {
555+
@Override
556+
protected NanoHTTPD.Response serve(NanoHTTPD.IHTTPSession session) {
557+
String site = getFirstParamValue(session, "site");
558+
if (site != null && !site.isEmpty()) {
559+
return newFixedLengthResponse(
560+
NanoHTTPD.Response.Status.OK,
561+
NanoHTTPD.MIME_HTML,
562+
body.replace(CONTENT_TOKEN, site));
563+
}
564+
return newFixedLengthResponse("<html><body></body></html>");
565+
}
566+
});
567+
HttpMessage msg = getHttpMessage(test + "?site=xxx");
568+
rule.init(msg, parent);
569+
// When
570+
rule.scan();
571+
// Then
572+
assertThat(alertsRaised.size(), equalTo(0));
573+
}
574+
534575
private static Stream<Arguments> createJsMethodBooleanPairs() {
535576
return Stream.of(
536577
Arguments.of("location.reload", true),

0 commit comments

Comments
 (0)