Skip to content

Commit 8b3650b

Browse files
authored
Merge pull request #6944 from kingthorin/sess-mgmt
authhelper: Session Detection rule performance fixes
2 parents 56fca64 + 3db8317 commit 8b3650b

File tree

11 files changed

+219
-81
lines changed

11 files changed

+219
-81
lines changed

addOns/authhelper/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1212
- Fail the Microsoft login if not able to perform all the expected steps.
1313
- Track GWT headers.
1414
- Handle additional exceptions when processing JSON authentication components.
15+
- Improved performance of the Session Detection scan rule.
1516

1617
### Fixed
1718
- Do not include known authentication providers in context.

addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AuthUtils.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ public void notifyMessageReceived(HttpMessage message) {
199199

200200
private static long timeToWaitMs = TimeUnit.SECONDS.toMillis(5);
201201

202-
@Setter private static HistoryProvider historyProvider = new HistoryProvider();
202+
@Setter
203+
private static HistoryProvider historyProvider = ExtensionAuthhelper.getHistoryProvider();
203204

204205
/**
205206
* These are session tokens that have been seen in responses but not yet seen in use. When they

addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/ExtensionAuthhelper.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Set;
3434
import java.util.stream.Stream;
3535
import javax.swing.ImageIcon;
36+
import lombok.Getter;
3637
import org.apache.commons.configuration.Configuration;
3738
import org.apache.commons.lang3.StringUtils;
3839
import org.apache.commons.text.StringEscapeUtils;
@@ -108,6 +109,8 @@ public class ExtensionAuthhelper extends ExtensionAdaptor {
108109

109110
public static final Set<Integer> HISTORY_TYPES_SET = Set.of(HISTORY_TYPES);
110111

112+
@Getter private static HistoryProvider historyProvider = new HistoryProvider();
113+
111114
private ZapMenuItem authTesterMenu;
112115
private AuthTestDialog authTestDialog;
113116

@@ -212,6 +215,7 @@ public void destroy() {
212215
@Override
213216
public void hook(ExtensionHook extensionHook) {
214217
extensionHook.addSessionListener(new AuthSessionChangedListener());
218+
extensionHook.addSessionListener(historyProvider);
215219
extensionHook.addHttpSenderListener(authHeaderTracker);
216220
extensionHook.addOptionsParamSet(getParam());
217221
if (hasView()) {

addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/HistoryProvider.java

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,54 @@
1919
*/
2020
package org.zaproxy.addon.authhelper;
2121

22+
import java.net.URLEncoder;
23+
import java.nio.charset.StandardCharsets;
24+
import java.sql.PreparedStatement;
25+
import java.sql.ResultSet;
26+
import java.sql.SQLException;
2227
import java.util.ArrayList;
2328
import java.util.List;
2429
import java.util.Optional;
30+
import org.apache.commons.text.StringEscapeUtils;
2531
import org.apache.logging.log4j.LogManager;
2632
import org.apache.logging.log4j.Logger;
33+
import org.parosproxy.paros.control.Control.Mode;
2734
import org.parosproxy.paros.core.scanner.Alert;
2835
import org.parosproxy.paros.db.DatabaseException;
36+
import org.parosproxy.paros.db.paros.ParosDatabaseServer;
37+
import org.parosproxy.paros.extension.SessionChangedListener;
2938
import org.parosproxy.paros.extension.history.ExtensionHistory;
3039
import org.parosproxy.paros.model.HistoryReference;
40+
import org.parosproxy.paros.model.Model;
41+
import org.parosproxy.paros.model.Session;
3142
import org.parosproxy.paros.network.HttpMalformedHeaderException;
3243
import org.parosproxy.paros.network.HttpMessage;
3344
import org.zaproxy.zap.authentication.AuthenticationHelper;
3445

3546
/** A very thin layer on top of the History functionality, to make testing easier. */
36-
public class HistoryProvider {
47+
public class HistoryProvider implements SessionChangedListener {
3748

3849
private static final int MAX_NUM_RECORDS_TO_CHECK = 200;
3950

4051
private static final Logger LOGGER = LogManager.getLogger(HistoryProvider.class);
4152

53+
private static final String QUERY_SESS_MGMT_TOKEN_MSG_IDS =
54+
"""
55+
SELECT HISTORYID FROM HISTORY
56+
WHERE HISTORYID BETWEEN ? AND ?
57+
AND (
58+
POSITION(? IN RESHEADER) > 0
59+
OR POSITION(? IN RESBODY) > 0
60+
OR POSITION(? IN REQHEADER) > 0
61+
OR POSITION(? IN REQHEADER) > 0 -- URLEncoded
62+
OR POSITION(? IN RESBODY) > 0 -- JSONEscaped
63+
)
64+
ORDER BY HISTORYID DESC
65+
""";
66+
67+
private ParosDatabaseServer pds;
68+
private boolean server;
69+
4270
private ExtensionHistory extHist;
4371

4472
private ExtensionHistory getExtHistory() {
@@ -61,21 +89,66 @@ public HttpMessage getHttpMessage(int historyId)
6189
return null;
6290
}
6391

92+
/**
93+
* The query is ordered DESCending so the List and subsequent processing should be newest
94+
* message first.
95+
*/
96+
List<Integer> getMessageIds(int first, int last, String value) {
97+
if (!server) {
98+
server = true;
99+
if (Model.getSingleton().getDb().getDatabaseServer()
100+
instanceof ParosDatabaseServer pdbs) {
101+
pds = pdbs;
102+
} else {
103+
LOGGER.warn("Unsupported Database Server.");
104+
}
105+
}
106+
if (pds == null) {
107+
return List.of();
108+
}
109+
110+
try (PreparedStatement psGetHistory =
111+
pds.getSingletonConnection().prepareStatement(QUERY_SESS_MGMT_TOKEN_MSG_IDS)) {
112+
psGetHistory.setInt(1, first);
113+
psGetHistory.setInt(2, last);
114+
psGetHistory.setString(3, value);
115+
psGetHistory.setBytes(4, value.getBytes(StandardCharsets.UTF_8));
116+
psGetHistory.setString(5, value);
117+
psGetHistory.setString(6, URLEncoder.encode(value, StandardCharsets.UTF_8));
118+
psGetHistory.setBytes(
119+
7, StringEscapeUtils.escapeJson(value).getBytes(StandardCharsets.UTF_8));
120+
121+
List<Integer> msgIds = new ArrayList<>();
122+
try (ResultSet rs = psGetHistory.executeQuery()) {
123+
while (rs.next()) {
124+
msgIds.add(rs.getInt("HISTORYID"));
125+
}
126+
} catch (SQLException e) {
127+
LOGGER.warn("Failed to process result set. {}", e.getMessage());
128+
}
129+
LOGGER.debug("Found: {} candidate messages for {}", msgIds.size(), value);
130+
return msgIds;
131+
} catch (SQLException e) {
132+
LOGGER.warn("Failed to prepare query.", e);
133+
return List.of();
134+
}
135+
}
136+
64137
public int getLastHistoryId() {
65138
return getExtHistory().getLastHistoryId();
66139
}
67140

68141
public SessionManagementRequestDetails findSessionTokenSource(String token, int firstId) {
69142
int lastId = getLastHistoryId();
70143
if (firstId == -1) {
71-
firstId = Math.max(0, lastId - MAX_NUM_RECORDS_TO_CHECK);
144+
firstId = Math.max(1, lastId - MAX_NUM_RECORDS_TO_CHECK);
72145
}
73146

74147
LOGGER.debug("Searching for session token from {} down to {} ", lastId, firstId);
75148

76-
for (int i = lastId; i >= firstId; i--) {
149+
for (int id : getMessageIds(firstId, lastId, token)) {
77150
try {
78-
HttpMessage msg = getHttpMessage(i);
151+
HttpMessage msg = getHttpMessage(id);
79152
if (msg == null) {
80153
continue;
81154
}
@@ -99,4 +172,25 @@ public SessionManagementRequestDetails findSessionTokenSource(String token, int
99172
}
100173
return null;
101174
}
175+
176+
@Override
177+
public void sessionChanged(Session session) {
178+
pds = null;
179+
server = false;
180+
}
181+
182+
@Override
183+
public void sessionAboutToChange(Session session) {
184+
// Nothing to do
185+
}
186+
187+
@Override
188+
public void sessionScopeChanged(Session session) {
189+
// Nothing to do
190+
}
191+
192+
@Override
193+
public void sessionModeChanged(Mode mode) {
194+
// Nothing to do
195+
}
102196
}

addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/SessionManagementRequestDetails.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,20 @@ public List<SessionToken> getTokens() {
4646
public int getConfidence() {
4747
return confidence;
4848
}
49+
50+
@Override
51+
public String toString() {
52+
String historyIdStr =
53+
msg.getHistoryRef() != null
54+
? String.valueOf(msg.getHistoryRef().getHistoryId())
55+
: "N/A";
56+
return "MsgID: "
57+
+ historyIdStr
58+
+ " tokens ("
59+
+ tokens.size()
60+
+ "): "
61+
+ tokens
62+
+ " confidence: "
63+
+ confidence;
64+
}
4965
}

addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/SessionToken.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,9 @@ private static int compareStrings(String string, String otherString) {
122122
}
123123
return string.compareTo(otherString);
124124
}
125+
126+
@Override
127+
public String toString() {
128+
return "Source: " + source + " key: " + key + " value: " + value;
129+
}
125130
}

addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/internal/ClientSideHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public final class ClientSideHandler implements HttpMessageHandler {
6060
private AuthRequestDetails authReq;
6161
private int firstHrefId;
6262

63-
@Setter private HistoryProvider historyProvider = new HistoryProvider();
63+
@Setter private HistoryProvider historyProvider = ExtensionAuthhelper.getHistoryProvider();
6464

6565
public ClientSideHandler(User user) {
6666
this.user = user;

addOns/authhelper/src/test/java/org/zaproxy/addon/authhelper/AuthUtilsUnitTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ void setUp() throws Exception {
106106
setUpZap();
107107

108108
mockMessages(new ExtensionAuthhelper());
109+
AuthUtils.setHistoryProvider(new TestHistoryProvider());
109110
}
110111

111112
@AfterEach

addOns/authhelper/src/test/java/org/zaproxy/addon/authhelper/SessionDetectionScanRuleUnitTest.java

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import static org.mockito.Mockito.verify;
3232
import static org.mockito.Mockito.withSettings;
3333

34-
import java.util.ArrayList;
3534
import java.util.Arrays;
3635
import java.util.List;
3736
import org.junit.jupiter.api.AfterEach;
@@ -43,9 +42,7 @@
4342
import org.parosproxy.paros.Constant;
4443
import org.parosproxy.paros.control.Control;
4544
import org.parosproxy.paros.core.scanner.Alert;
46-
import org.parosproxy.paros.db.DatabaseException;
4745
import org.parosproxy.paros.extension.ExtensionLoader;
48-
import org.parosproxy.paros.model.HistoryReference;
4946
import org.parosproxy.paros.model.Model;
5047
import org.parosproxy.paros.model.Session;
5148
import org.parosproxy.paros.network.HttpHeader;
@@ -67,7 +64,6 @@
6764
/** Unit test for {@link SessionDetectionScanRule}. */
6865
class SessionDetectionScanRuleUnitTest extends PassiveScannerTest<SessionDetectionScanRule> {
6966

70-
private List<HttpMessage> history;
7167
private HistoryProvider historyProvider;
7268

7369
@Override
@@ -107,7 +103,6 @@ void shouldSetHeaderBasedSessionManagment() throws Exception {
107103
given(session.getContextsForUrl(anyString())).willReturn(Arrays.asList(context));
108104
given(model.getSession()).willReturn(session);
109105

110-
history = new ArrayList<>();
111106
historyProvider = new TestHistoryProvider();
112107
AuthUtils.setHistoryProvider(historyProvider);
113108

@@ -204,7 +199,6 @@ void shouldDetectBodgeitSession() throws Exception {
204199
DiagnosticDataLoader.loadTestData(
205200
this.getResourcePath("internal/bodgeit.diags").toFile());
206201

207-
history = new ArrayList<>();
208202
historyProvider = new TestHistoryProvider();
209203
AuthUtils.setHistoryProvider(historyProvider);
210204
// When
@@ -228,7 +222,6 @@ void shouldDetectBodgeitSession() throws Exception {
228222
@Test
229223
void shouldDetectCtflearnSession() throws Exception {
230224
// Given
231-
history = new ArrayList<>();
232225
historyProvider = new TestHistoryProvider();
233226
AuthUtils.setHistoryProvider(historyProvider);
234227

@@ -267,7 +260,6 @@ void shouldDetectCtflearnSession() throws Exception {
267260
@Test
268261
void shouldDetectDefthewebSession() throws Exception {
269262
// Given
270-
history = new ArrayList<>();
271263
historyProvider = new TestHistoryProvider();
272264
AuthUtils.setHistoryProvider(historyProvider);
273265

@@ -295,7 +287,6 @@ void shouldDetectDefthewebSession() throws Exception {
295287
@Test
296288
void shouldDetectGinnjuiceSession() throws Exception {
297289
// Given
298-
history = new ArrayList<>();
299290
historyProvider = new TestHistoryProvider();
300291
AuthUtils.setHistoryProvider(historyProvider);
301292

@@ -325,7 +316,6 @@ void shouldDetectGinnjuiceSession() throws Exception {
325316
@Test
326317
void shouldDetectInfosecexSession() throws Exception {
327318
// Given
328-
history = new ArrayList<>();
329319
historyProvider = new TestHistoryProvider();
330320
AuthUtils.setHistoryProvider(historyProvider);
331321

@@ -372,7 +362,6 @@ void shouldFindTokenWhenOneIsPreviouslyUnknown() throws Exception {
372362
extensionLoader =
373363
mock(ExtensionLoader.class, withSettings().strictness(Strictness.LENIENT));
374364

375-
history = new ArrayList<>();
376365
historyProvider = new TestHistoryProvider();
377366
AuthUtils.setHistoryProvider(historyProvider);
378367

@@ -470,27 +459,4 @@ void shouldIgnoreUnknownOrUnwantedContentTypes(String contentType)
470459
// Then
471460
assertThat(alertsRaised, hasSize(0));
472461
}
473-
474-
class TestHistoryProvider extends HistoryProvider {
475-
@Override
476-
public void addAuthMessageToHistory(HttpMessage msg) {
477-
history.add(msg);
478-
int id = history.size() - 1;
479-
HistoryReference href =
480-
mock(HistoryReference.class, withSettings().strictness(Strictness.LENIENT));
481-
given(href.getHistoryId()).willReturn(id);
482-
msg.setHistoryRef(href);
483-
}
484-
485-
@Override
486-
public HttpMessage getHttpMessage(int historyId)
487-
throws HttpMalformedHeaderException, DatabaseException {
488-
return history.get(historyId);
489-
}
490-
491-
@Override
492-
public int getLastHistoryId() {
493-
return history.size() - 1;
494-
}
495-
}
496462
}

0 commit comments

Comments
 (0)