Skip to content

Commit f81ca84

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 f81ca84

File tree

4 files changed

+114
-31
lines changed

4 files changed

+114
-31
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: 67 additions & 30 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",
@@ -130,6 +131,7 @@ public class PathTraversalScanRule extends AbstractAppParamPlugin
130131
// Dot used to match 'x' or '!' (used in AIX)
131132
private static final ContentsMatcher NIX_PATTERN =
132133
new PatternContentsMatcher(Pattern.compile("root:.:0:0"));
134+
private static final String NIX_DIR_EVIDENCE = "etc";
133135
private static final String[] NIX_LOCAL_FILE_TARGETS = {
134136
// Absolute file retrieval
135137
"/etc/passwd",
@@ -191,6 +193,8 @@ public class PathTraversalScanRule extends AbstractAppParamPlugin
191193
*/
192194
private static final String[] LOCAL_FILE_RELATIVE_PREFIXES = {"", "/", "\\"};
193195

196+
private static final List<String> DIR_EVIDENCE_LIST =
197+
List.of(NIX_DIR_EVIDENCE, WIN_DIR_EVIDENCE);
194198
/*
195199
* details of the vulnerability which we are attempting to find
196200
*/
@@ -303,6 +307,9 @@ public void scan(HttpMessage msg, String param, String value) {
303307

304308
// Check 1: Start detection for Windows patterns
305309
if (inScope(Tech.Windows)) {
310+
if (!isGoodCandidate(getBaseMsg(), Tech.Windows)) {
311+
return;
312+
}
306313

307314
for (int h = 0; h < winCount; h++) {
308315

@@ -341,7 +348,9 @@ public void scan(HttpMessage msg, String param, String value) {
341348

342349
// Check 2: Start detection for *NIX patterns
343350
if (inScope(Tech.Linux) || inScope(Tech.MacOS)) {
344-
351+
if (!isGoodCandidate(getBaseMsg(), Tech.Linux)) {
352+
return;
353+
}
345354
for (int h = 0; h < nixCount; h++) {
346355

347356
// Check if a there was a finding or the scan has been stopped
@@ -569,6 +578,18 @@ public void scan(HttpMessage msg, String param, String value) {
569578
}
570579
}
571580

581+
private static boolean isGoodCandidate(HttpMessage msg, Tech targetTech) {
582+
if (targetTech == Tech.Windows) {
583+
return DirNamesContentsMatcher.matchWinDirectories(msg.getResponseBody().toString())
584+
== null;
585+
}
586+
if (targetTech == Tech.Linux) {
587+
return DirNamesContentsMatcher.matchNixDirectories(msg.getResponseBody().toString())
588+
== null;
589+
}
590+
return false;
591+
}
592+
572593
private boolean sendAndCheckPayload(
573594
String param, String newValue, ContentsMatcher contentsMatcher, int check)
574595
throws IOException {
@@ -658,12 +679,23 @@ private AlertBuilder createUnmatchedAlert(String param, String attack) {
658679

659680
private AlertBuilder createMatchedAlert(
660681
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);
682+
AlertBuilder builder =
683+
newAlert()
684+
.setConfidence(Alert.CONFIDENCE_MEDIUM)
685+
.setParam(param)
686+
.setAttack(attack)
687+
.setEvidence(evidence)
688+
.setAlertRef(getId() + "-" + check);
689+
if (DIR_EVIDENCE_LIST.contains(evidence)) {
690+
builder.setOtherInfo(
691+
Constant.messages.getString(
692+
MESSAGE_PREFIX + "info",
693+
evidence,
694+
evidence.equals(WIN_DIR_EVIDENCE)
695+
? DirNamesContentsMatcher.WIN_MATCHES
696+
: DirNamesContentsMatcher.NIX_MATCHES));
697+
}
698+
return builder;
667699
}
668700

669701
@Override
@@ -705,6 +737,26 @@ public String match(String contents) {
705737

706738
private static class DirNamesContentsMatcher implements ContentsMatcher {
707739

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

717769
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";
770+
if (PROC_PATT.matcher(contents).find()
771+
&& ETC_PATT.matcher(contents).find()
772+
&& BOOT_PATT.matcher(contents).find()
773+
&& TMP_PATT.matcher(contents).find()
774+
&& HOME_PATT.matcher(contents).find()) {
775+
return NIX_DIR_EVIDENCE;
739776
}
740777

741778
return null;
742779
}
743780

744781
private static String matchWinDirectories(String contents) {
745-
if (contents.contains("Windows")
782+
if (contents.contains(WIN_DIR_EVIDENCE)
746783
&& Pattern.compile("Program\\sFiles").matcher(contents).find()) {
747-
return "Windows";
784+
return WIN_DIR_EVIDENCE;
748785
}
749786

750787
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+
"foo etc root tmp bin boot dev home mnt opt proc bar, 4",
395+
"foo Program Files Users Windows bar, 0",
396+
"foo etc root tmp bin boot dev home mnt opt proc Program Files Users Windows bar, 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)