Skip to content

Commit 88554d8

Browse files
committed
ascanrules: Path Traversal add details for dir match Alerts & reduce FPs
- CHANGELOG > Added change note. - Message.properties > Added key/value pair supporting the new Alert details. - PathTraversalScanRule > Updated to include Other Info on Alerts when applicable, and pre-check the original message response to reduce false positives. - PathTraversalScanRuleUnitTest > Updated to assert Other Info or lack thereof where applicable, also assure appropriate skipping due to pre-conditions. Signed-off-by: kingthorin <[email protected]>
1 parent 3159ed2 commit 88554d8

File tree

4 files changed

+112
-30
lines changed

4 files changed

+112
-30
lines changed

addOns/ascanrules/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ All notable changes to this add-on will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
55

66
## Unreleased
7-
7+
### Changed
8+
- The Path Traversal scan rule now includes further details when directory matches are made and pre-checks the original message to reduce false positives (Issue 8379).
89

910
## [71] - 2025-03-04
1011
### Fixed

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

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public class PathTraversalScanRule extends AbstractAppParamPlugin
8181
*/
8282
private static final ContentsMatcher WIN_PATTERN =
8383
new PatternContentsMatcher(Pattern.compile("\\[drivers\\]"));
84+
private static final String WIN_DIR_EVIDENCE = "Windows";
8485
private static final String[] WIN_LOCAL_FILE_TARGETS = {
8586
// Absolute Windows file retrieval (we suppose C:\\)
8687
"c:/Windows/system.ini",
@@ -128,8 +129,10 @@ public class PathTraversalScanRule extends AbstractAppParamPlugin
128129
* Unix/Linux/etc. local file targets and detection pattern
129130
*/
130131
// Dot used to match 'x' or '!' (used in AIX)
132+
private static final List<Tech> NIX_TECHS = List.of(Tech.Linux, Tech.MacOS);
131133
private static final ContentsMatcher NIX_PATTERN =
132134
new PatternContentsMatcher(Pattern.compile("root:.:0:0"));
135+
private static final String NIX_DIR_EVIDENCE = "etc";
133136
private static final String[] NIX_LOCAL_FILE_TARGETS = {
134137
// Absolute file retrieval
135138
"/etc/passwd",
@@ -191,6 +194,8 @@ public class PathTraversalScanRule extends AbstractAppParamPlugin
191194
*/
192195
private static final String[] LOCAL_FILE_RELATIVE_PREFIXES = {"", "/", "\\"};
193196

197+
private static final List<String> DIR_EVIDENCE_LIST =
198+
List.of(NIX_DIR_EVIDENCE, WIN_DIR_EVIDENCE);
194199
/*
195200
* details of the vulnerability which we are attempting to find
196201
*/
@@ -303,6 +308,9 @@ public void scan(HttpMessage msg, String param, String value) {
303308

304309
// Check 1: Start detection for Windows patterns
305310
if (inScope(Tech.Windows)) {
311+
if (!isGoodCandidate(getBaseMsg(), Tech.Windows)) {
312+
return;
313+
}
306314

307315
for (int h = 0; h < winCount; h++) {
308316

@@ -569,6 +577,18 @@ public void scan(HttpMessage msg, String param, String value) {
569577
}
570578
}
571579

580+
private static boolean isGoodCandidate(HttpMessage msg, Tech targetTech) {
581+
if (targetTech == Tech.Windows) {
582+
return DirNamesContentsMatcher.matchWinDirectories(msg.getResponseBody().toString())
583+
== null;
584+
}
585+
if (NIX_TECHS.contains(targetTech)) {
586+
return DirNamesContentsMatcher.matchNixDirectories(msg.getResponseBody().toString())
587+
== null;
588+
}
589+
return false;
590+
}
591+
572592
private boolean sendAndCheckPayload(
573593
String param, String newValue, ContentsMatcher contentsMatcher, int check)
574594
throws IOException {
@@ -658,12 +678,23 @@ private AlertBuilder createUnmatchedAlert(String param, String attack) {
658678

659679
private AlertBuilder createMatchedAlert(
660680
String param, String attack, String evidence, int check) {
661-
return newAlert()
662-
.setConfidence(Alert.CONFIDENCE_MEDIUM)
663-
.setParam(param)
664-
.setAttack(attack)
665-
.setEvidence(evidence)
666-
.setAlertRef(getId() + "-" + check);
681+
AlertBuilder builder =
682+
newAlert()
683+
.setConfidence(Alert.CONFIDENCE_MEDIUM)
684+
.setParam(param)
685+
.setAttack(attack)
686+
.setEvidence(evidence)
687+
.setAlertRef(getId() + "-" + check);
688+
if (DIR_EVIDENCE_LIST.contains(evidence)) {
689+
builder.setOtherInfo(
690+
Constant.messages.getString(
691+
MESSAGE_PREFIX + "info",
692+
evidence,
693+
evidence.equals(WIN_DIR_EVIDENCE)
694+
? DirNamesContentsMatcher.WIN_MATCHES
695+
: DirNamesContentsMatcher.NIX_MATCHES));
696+
}
697+
return builder;
667698
}
668699

669700
@Override
@@ -705,6 +736,26 @@ public String match(String contents) {
705736

706737
private static class DirNamesContentsMatcher implements ContentsMatcher {
707738

739+
private static final String NIX_MATCHES =
740+
String.join(", ", List.of("proc", NIX_DIR_EVIDENCE, "boot", "tmp", "home"));
741+
private static final String WIN_MATCHES =
742+
String.join(", ", List.of(WIN_DIR_EVIDENCE, "Program Files"));
743+
private static final Pattern PROC_PATT =
744+
Pattern.compile(
745+
"(?:^|\\W)proc(?:\\W|$)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
746+
private static final Pattern ETC_PATT =
747+
Pattern.compile(
748+
"(?:^|\\W)etc(?:\\W|$)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
749+
private static final Pattern BOOT_PATT =
750+
Pattern.compile(
751+
"(?:^|\\W)boot(?:\\W|$)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
752+
private static final Pattern TMP_PATT =
753+
Pattern.compile(
754+
"(?:^|\\W)tmp(?:\\W|$)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
755+
private static final Pattern HOME_PATT =
756+
Pattern.compile(
757+
"(?:^|\\W)home(?:\\W|$)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
758+
708759
@Override
709760
public String match(String contents) {
710761
String result = matchNixDirectories(contents);
@@ -715,36 +766,21 @@ public String match(String contents) {
715766
}
716767

717768
private static String matchNixDirectories(String contents) {
718-
Pattern procPattern =
719-
Pattern.compile("(?:^|\\W)proc(?:\\W|$)", Pattern.CASE_INSENSITIVE);
720-
Pattern etcPattern = Pattern.compile("(?:^|\\W)etc(?:\\W|$)", Pattern.CASE_INSENSITIVE);
721-
Pattern bootPattern =
722-
Pattern.compile("(?:^|\\W)boot(?:\\W|$)", Pattern.CASE_INSENSITIVE);
723-
Pattern tmpPattern = Pattern.compile("(?:^|\\W)tmp(?:\\W|$)", Pattern.CASE_INSENSITIVE);
724-
Pattern homePattern =
725-
Pattern.compile("(?:^|\\W)home(?:\\W|$)", Pattern.CASE_INSENSITIVE);
726-
727-
Matcher procMatcher = procPattern.matcher(contents);
728-
Matcher etcMatcher = etcPattern.matcher(contents);
729-
Matcher bootMatcher = bootPattern.matcher(contents);
730-
Matcher tmpMatcher = tmpPattern.matcher(contents);
731-
Matcher homeMatcher = homePattern.matcher(contents);
732-
733-
if (procMatcher.find()
734-
&& etcMatcher.find()
735-
&& bootMatcher.find()
736-
&& tmpMatcher.find()
737-
&& homeMatcher.find()) {
738-
return "etc";
769+
if (PROC_PATT.matcher(contents).find()
770+
&& ETC_PATT.matcher(contents).find()
771+
&& BOOT_PATT.matcher(contents).find()
772+
&& TMP_PATT.matcher(contents).find()
773+
&& HOME_PATT.matcher(contents).find()) {
774+
return NIX_DIR_EVIDENCE;
739775
}
740776

741777
return null;
742778
}
743779

744780
private static String matchWinDirectories(String contents) {
745-
if (contents.contains("Windows")
781+
if (contents.contains(WIN_DIR_EVIDENCE)
746782
&& Pattern.compile("Program\\sFiles").matcher(contents).find()) {
747-
return "Windows";
783+
return WIN_DIR_EVIDENCE;
748784
}
749785

750786
return null;

addOns/ascanrules/src/main/resources/org/zaproxy/zap/extension/ascanrules/resources/Messages.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ ascanrules.parametertamper.desc = Parameter manipulation caused an error page or
114114
ascanrules.parametertamper.name = Parameter Tampering
115115
ascanrules.parametertamper.soln = Identify the cause of the error and fix it. Do not trust client side input and enforce a tight check in the server side. Besides, catch the exception properly. Use a generic 500 error page for internal server error.
116116

117+
ascanrules.pathtraversal.info = While the evidence field indicates {0}, the rule actually checked that the response contains matches for all of the following: {1}.
117118
ascanrules.pathtraversal.name = Path Traversal
118119

119120
ascanrules.payloader.desc = Provides support for custom payloads in scan rules.

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import static fi.iki.elonen.NanoHTTPD.newFixedLengthResponse;
2323
import static org.hamcrest.MatcherAssert.assertThat;
24+
import static org.hamcrest.Matchers.emptyString;
2425
import static org.hamcrest.Matchers.equalTo;
2526
import static org.hamcrest.Matchers.greaterThan;
2627
import static org.hamcrest.Matchers.hasSize;
@@ -34,6 +35,7 @@
3435
import org.apache.commons.lang3.ArrayUtils;
3536
import org.junit.jupiter.api.Test;
3637
import org.junit.jupiter.params.ParameterizedTest;
38+
import org.junit.jupiter.params.provider.CsvSource;
3739
import org.junit.jupiter.params.provider.EnumSource;
3840
import org.junit.jupiter.params.provider.ValueSource;
3941
import org.parosproxy.paros.core.scanner.Alert;
@@ -169,6 +171,13 @@ void shouldAlertIfAttackResponseListsWindowsDirectories() throws Exception {
169171
assertThat(alertsRaised.get(0).getAttack(), is(equalTo("c:/")));
170172
assertThat(alertsRaised.get(0).getRisk(), is(equalTo(Alert.RISK_HIGH)));
171173
assertThat(alertsRaised.get(0).getConfidence(), is(equalTo(Alert.CONFIDENCE_MEDIUM)));
174+
assertThat(
175+
alertsRaised.get(0).getOtherInfo(),
176+
is(
177+
equalTo(
178+
"While the evidence field indicates Windows, the rule actually "
179+
+ "checked that the response contains matches for all of the "
180+
+ "following: Windows, Program Files.")));
172181
assertThat(alertsRaised.get(0).getAlertRef(), is(equalTo("6-3")));
173182
}
174183

@@ -187,6 +196,13 @@ void shouldAlertIfAttackResponseListsLinuxDirectories() throws Exception {
187196
assertThat(alertsRaised.get(0).getAttack(), is(equalTo("/")));
188197
assertThat(alertsRaised.get(0).getRisk(), is(equalTo(Alert.RISK_HIGH)));
189198
assertThat(alertsRaised.get(0).getConfidence(), is(equalTo(Alert.CONFIDENCE_MEDIUM)));
199+
assertThat(
200+
alertsRaised.get(0).getOtherInfo(),
201+
is(
202+
equalTo(
203+
"While the evidence field indicates etc, the rule actually "
204+
+ "checked that the response contains matches for all of the "
205+
+ "following: proc, etc, boot, tmp, home.")));
190206
assertThat(alertsRaised.get(0).getAlertRef(), is(equalTo("6-3")));
191207
}
192208

@@ -205,6 +221,13 @@ void shouldAlertIfAttackResponseListsLinuxDirectoriesInPlainText() throws Except
205221
assertThat(alertsRaised.get(0).getAttack(), is(equalTo("/")));
206222
assertThat(alertsRaised.get(0).getRisk(), is(equalTo(Alert.RISK_HIGH)));
207223
assertThat(alertsRaised.get(0).getConfidence(), is(equalTo(Alert.CONFIDENCE_MEDIUM)));
224+
assertThat(
225+
alertsRaised.get(0).getOtherInfo(),
226+
is(
227+
equalTo(
228+
"While the evidence field indicates etc, the rule actually "
229+
+ "checked that the response contains matches for all of the "
230+
+ "following: proc, etc, boot, tmp, home.")));
208231
assertThat(alertsRaised.get(0).getAlertRef(), is(equalTo("6-3")));
209232
}
210233

@@ -278,6 +301,7 @@ void shouldRaiseAlertIfResponseHasPasswdFileContentAndPayloadIsNullByteBased()
278301
// Then
279302
assertThat(alertsRaised, hasSize(1));
280303
assertThat(alertsRaised.get(0).getAlertRef(), is(equalTo("6-2")));
304+
assertThat(alertsRaised.get(0).getOtherInfo(), is(emptyString()));
281305
}
282306

283307
@Test
@@ -294,6 +318,7 @@ void shouldRaiseAlertIfResponseHasSystemINIFileContentAndPayloadIsNullByteBased(
294318
// Then
295319
assertThat(alertsRaised, hasSize(1));
296320
assertThat(alertsRaised.get(0).getAlertRef(), is(equalTo("6-1")));
321+
assertThat(alertsRaised.get(0).getOtherInfo(), is(emptyString()));
297322
}
298323

299324
@ParameterizedTest
@@ -314,6 +339,7 @@ void shouldAlertOnCheckFiveBelowHighThresholdUnderValidConditions(AlertThreshold
314339
assertThat(alertsRaised, hasSize(1));
315340
assertThat(alertsRaised.get(0).getConfidence(), is(equalTo(Alert.CONFIDENCE_LOW)));
316341
assertThat(alertsRaised.get(0).getAlertRef(), is(equalTo("6-5")));
342+
assertThat(alertsRaised.get(0).getOtherInfo(), is(emptyString()));
317343
}
318344

319345
@Test
@@ -362,6 +388,24 @@ void shouldNotAlertOnCheckFiveAtLowThresholdUnderInvalidConditions(String errorT
362388
assertThat(alertsRaised, hasSize(0));
363389
}
364390

391+
@ParameterizedTest
392+
@CsvSource({
393+
// Windows will always happen before ignoring Linux, if only Linux pre-check matches
394+
"etc root tmp bin boot dev home mnt opt proc, 11",
395+
"Program Files Users Windows, 0",
396+
"etc root tmp bin boot dev home mnt opt proc Program Files Users Windows, 0"
397+
})
398+
void shouldSkipIfResponseHasEvidenceToStartWith(String content, int expected) throws Exception {
399+
// Given
400+
HttpMessage msg = getHttpMessage("/?p=v");
401+
msg.setResponseBody(content);
402+
rule.init(msg, parent);
403+
// When
404+
rule.scan();
405+
// Then
406+
assertThat(httpMessagesSent, hasSize(equalTo(expected)));
407+
}
408+
365409
private abstract static class ListDirsOnAttack extends NanoServerHandler {
366410

367411
private final String param;

0 commit comments

Comments
 (0)