Skip to content

Commit 6e69a14

Browse files
committed
authhelper: auth detection rewrite
Signed-off-by: Simon Bennetts <psiinon@gmail.com>
1 parent bdf2d49 commit 6e69a14

21 files changed

+1266
-87
lines changed

addOns/authhelper/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1313
- Add any session related cookies which are not being tracked.
1414
- Ignore non proxied requests in auth tester diagnostics.
1515
- Replace credentials with special tokens.
16+
- Rewrite of the auth request detection code to handle more cases.
17+
- Add domain to context if creds posted to it and using using auto-detect for session management.
1618

1719
### Fixed
1820
- Bug where some of the data structures were not being reset when the session changed.

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -796,7 +796,9 @@ public static List<Pair<String, String>> getHeaderTokens(
796796
}
797797
if (incCookies) {
798798
for (HttpCookie cookie : msg.getRequestHeader().getHttpCookies()) {
799-
if (cookie.getValue().contains(token.getValue())) {
799+
if (!(SessionToken.COOKIE_SOURCE.equals(token.getSource())
800+
&& cookie.getName().equals(token.getKey()))
801+
&& cookie.getValue().contains(token.getValue())) {
800802
String hv =
801803
cookie.getValue()
802804
.replace(token.getValue(), "{%" + token.getToken() + "%}");

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,10 @@ public BrowserBasedAuthenticationMethodType(HttpSender httpSender) {
140140
this.httpSender = httpSender;
141141
}
142142

143-
private Server getProxy(Context context) {
143+
private Server getProxy(User user) {
144144
if (proxy == null) {
145145
ExtensionNetwork extNet = AuthUtils.getExtension(ExtensionNetwork.class);
146-
handler = new ClientSideHandler(context);
146+
handler = new ClientSideHandler(user);
147147
proxy =
148148
extNet.createHttpServer(
149149
HttpServerConfig.builder()
@@ -314,7 +314,7 @@ private WebSession authenticateImpl(
314314
.getExtension(ExtensionSelenium.class);
315315

316316
try {
317-
proxyPort = getProxy(user.getContext()).start(proxyHost, 0);
317+
proxyPort = getProxy(user).start(proxyHost, 0);
318318

319319
WebDriver wd = null;
320320
try {

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ public class ClientScriptBasedAuthenticationMethodType extends ScriptBasedAuthen
8888

8989
public ClientScriptBasedAuthenticationMethodType() {}
9090

91-
private HttpMessageHandler getHandler(Context context) {
91+
private HttpMessageHandler getHandler(User user) {
9292
if (handler == null) {
93-
handler = new ClientSideHandler(context);
93+
handler = new ClientSideHandler(user);
9494
}
9595
return handler;
9696
}
@@ -334,7 +334,7 @@ public WebSession authenticate(
334334
}
335335

336336
if (authScript instanceof ZestAuthenticationRunner zestRunner) {
337-
zestRunner.registerHandler(getHandler(user.getContext()));
337+
zestRunner.registerHandler(getHandler(user));
338338
appendCloseStatements(zestRunner.getScript().getZestScript());
339339
} else {
340340
LOGGER.warn("Expected authScript to be a Zest script");

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

+202-60
Original file line numberDiff line numberDiff line change
@@ -20,38 +20,52 @@
2020
package org.zaproxy.addon.authhelper.internal;
2121

2222
import java.util.ArrayList;
23-
import java.util.Map;
23+
import java.util.Collection;
24+
import java.util.HashSet;
25+
import java.util.List;
2426
import java.util.Set;
27+
import java.util.regex.Matcher;
28+
import java.util.regex.Pattern;
29+
import lombok.Getter;
2530
import lombok.Setter;
26-
import org.apache.logging.log4j.Level;
31+
import org.apache.commons.httpclient.URIException;
32+
import org.apache.commons.lang3.StringUtils;
2733
import org.apache.logging.log4j.LogManager;
2834
import org.apache.logging.log4j.Logger;
29-
import org.parosproxy.paros.Constant;
30-
import org.parosproxy.paros.core.scanner.Alert;
35+
import org.parosproxy.paros.network.HttpHeader;
3136
import org.parosproxy.paros.network.HttpMessage;
3237
import org.parosproxy.paros.network.HttpRequestHeader;
33-
import org.parosproxy.paros.view.View;
38+
import org.parosproxy.paros.network.HttpStatusCode;
3439
import org.zaproxy.addon.authhelper.AuthUtils;
40+
import org.zaproxy.addon.authhelper.HeaderBasedSessionManagementMethodType;
3541
import org.zaproxy.addon.authhelper.HistoryProvider;
3642
import org.zaproxy.addon.authhelper.SessionManagementRequestDetails;
3743
import org.zaproxy.addon.authhelper.SessionToken;
3844
import org.zaproxy.addon.network.server.HttpMessageHandler;
3945
import org.zaproxy.addon.network.server.HttpMessageHandlerContext;
40-
import org.zaproxy.zap.model.Context;
46+
import org.zaproxy.zap.authentication.UsernamePasswordAuthenticationCredentials;
47+
import org.zaproxy.zap.model.SessionStructure;
48+
import org.zaproxy.zap.users.User;
49+
import org.zaproxy.zap.utils.Pair;
4150

4251
public final class ClientSideHandler implements HttpMessageHandler {
4352

4453
private static final Logger LOGGER = LogManager.getLogger(ClientSideHandler.class);
4554

46-
private final Context context;
47-
private HttpMessage authMsg;
55+
private final User user;
56+
private UsernamePasswordAuthenticationCredentials authCreds;
57+
private AuthRequestDetails authReq;
4858
private HttpMessage fallbackMsg;
4959
private int firstHrefId;
5060

5161
@Setter private HistoryProvider historyProvider = new HistoryProvider();
5262

53-
public ClientSideHandler(Context context) {
54-
this.context = context;
63+
public ClientSideHandler(User user) {
64+
this.user = user;
65+
if (user.getAuthenticationCredentials()
66+
instanceof UsernamePasswordAuthenticationCredentials authCreds) {
67+
this.authCreds = authCreds;
68+
}
5569
}
5670

5771
private boolean isPost(HttpMessage msg) {
@@ -64,81 +78,209 @@ public void handleMessage(HttpMessageHandlerContext ctx, HttpMessage msg) {
6478
if (ctx.isFromClient()) {
6579
return;
6680
}
81+
if (firstHrefId == 0 && msg.getHistoryRef() != null) {
82+
// Backstop for looping back through the history
83+
firstHrefId = msg.getHistoryRef().getHistoryId();
84+
}
6785

6886
historyProvider.addAuthMessageToHistory(msg);
6987

70-
if (!context.isIncluded(msg.getRequestHeader().getURI().toString())) {
71-
return;
88+
if (!user.getContext().isIncluded(msg.getRequestHeader().getURI().toString())) {
89+
String reqBody = msg.getRequestBody().toString();
90+
if (isPost(msg)
91+
&& authCreds != null
92+
&& StringUtils.isNotEmpty(authCreds.getUsername())
93+
&& StringUtils.isNotEmpty(authCreds.getPassword())
94+
&& reqBody.contains(authCreds.getUsername())
95+
&& reqBody.contains(authCreds.getPassword())
96+
&& AuthUtils.getSessionManagementDetailsForContext(user.getContext().getId())
97+
!= null) {
98+
// The app is sending user creds to another site. Assume this is part of the valid
99+
// auth flow and add to the context
100+
try {
101+
user.getContext()
102+
.addIncludeInContextRegex(SessionStructure.getHostName(msg) + ".*");
103+
} catch (URIException e) {
104+
// Very unexpected
105+
LOGGER.error(e.getMessage(), e);
106+
return;
107+
}
108+
} else {
109+
// Not in the context, no creds, not relevant
110+
return;
111+
}
72112
}
113+
AuthRequestDetails candidate = new AuthRequestDetails(msg);
73114

74-
if (isPost(msg)) {
75-
// Record the last in scope POST as a fallback
76-
fallbackMsg = msg;
115+
List<Pair<String, String>> headerConfigs = null;
116+
117+
if (user.getContext().getSessionManagementMethod()
118+
instanceof
119+
HeaderBasedSessionManagementMethodType.HeaderBasedSessionManagementMethod smgmt) {
120+
headerConfigs = smgmt.getHeaderConfigs();
77121
}
78122

79-
if (authMsg != null && isPost(authMsg) && !isPost(msg)) {
80-
// We have a better candidate
81-
return;
123+
if (candidate.isBetterThan(authReq, headerConfigs)) {
124+
LOGGER.debug(
125+
"Found better auth candidate {} {}",
126+
msg.getRequestHeader().getMethod(),
127+
msg.getRequestHeader().getURI());
128+
authReq = candidate;
82129
}
130+
83131
Set<SessionToken> reqSessionTokens = AuthUtils.getRequestSessionTokens(msg);
132+
Set<SessionToken> unkSessionTokens = new HashSet<>();
84133
for (SessionToken token : reqSessionTokens) {
85134
if (!SessionToken.COOKIE_SOURCE.equals(token.getSource())) {
86-
AuthUtils.recordRequestSessionToken(context, token.getKey(), token.getValue());
135+
AuthUtils.recordRequestSessionToken(
136+
user.getContext(), token.getKey(), token.getValue());
137+
}
138+
if (AuthUtils.containsSessionToken(token.getValue()) == null) {
139+
unkSessionTokens.add(token);
87140
}
88141
}
89-
90-
SessionManagementRequestDetails smReqDetails = null;
91-
Map<String, SessionToken> sessionTokens = AuthUtils.getResponseSessionTokens(msg);
92-
if (!sessionTokens.isEmpty()) {
93-
authMsg = msg;
94-
LOGGER.debug("Session token found in href {} {}", getHrefId(authMsg), isPost(msg));
95-
smReqDetails =
96-
new SessionManagementRequestDetails(
97-
authMsg,
98-
new ArrayList<>(sessionTokens.values()),
99-
Alert.CONFIDENCE_HIGH);
100-
} else {
101-
if (!reqSessionTokens.isEmpty()) {
102-
// The request has at least one auth token we missed - try
103-
// to find one of them
104-
for (SessionToken st : reqSessionTokens) {
105-
smReqDetails = AuthUtils.findSessionTokenSource(st.getValue(), firstHrefId);
106-
if (smReqDetails != null) {
107-
authMsg = smReqDetails.getMsg();
108-
LOGGER.debug("Session token found in href {}", getHrefId(authMsg));
109-
break;
110-
}
142+
for (SessionToken st : unkSessionTokens) {
143+
// See if we can find the reqs for the unknown session tokens, then see if they are
144+
// better than the current one
145+
SessionManagementRequestDetails smReqDetails =
146+
AuthUtils.findSessionTokenSource(st.getValue(), firstHrefId);
147+
if (smReqDetails != null) {
148+
candidate = new AuthRequestDetails(msg);
149+
if (candidate.isBetterThan(authReq, headerConfigs)) {
150+
LOGGER.debug(
151+
"Found better auth candidate {} {}",
152+
msg.getRequestHeader().getMethod(),
153+
msg.getRequestHeader().getURI());
154+
authReq = candidate;
111155
}
112156
}
113-
114-
if (authMsg != null && View.isInitialised()) {
115-
AuthUtils.logUserMessage(
116-
Level.INFO,
117-
Constant.messages.getString(
118-
"authhelper.auth.method.browser.output.sessionid", getHrefId(msg)));
119-
}
120-
}
121-
if (firstHrefId == 0 && msg.getHistoryRef() != null) {
122-
firstHrefId = msg.getHistoryRef().getHistoryId();
123157
}
124158
}
125159

126-
private String getHrefId(HttpMessage msg) {
127-
if (msg.getHistoryRef() != null) {
128-
return Integer.toString(msg.getHistoryRef().getHistoryId());
129-
}
130-
return "?";
131-
}
132-
133160
public HttpMessage getAuthMsg() {
134-
return authMsg;
161+
return authReq.getMsg();
135162
}
136163

137164
public void resetAuthMsg() {
138-
this.authMsg = null;
165+
this.authReq = null;
166+
}
167+
168+
protected static boolean isBetterThan(
169+
SessionManagementRequestDetails smrd1, SessionManagementRequestDetails smrd2) {
170+
if (smrd2 == null) {
171+
return true;
172+
}
173+
if (smrd1.getConfidence() > smrd2.getConfidence()) {
174+
return true;
175+
}
176+
if (smrd1.getConfidence() < smrd2.getConfidence()) {
177+
return false;
178+
}
179+
return smrd1.getTokens().size() > smrd2.getTokens().size();
139180
}
140181

141182
public HttpMessage getFallbackMsg() {
142183
return fallbackMsg;
143184
}
185+
186+
protected static List<Pair<String, String>> extractKeyValuePairs(String input) {
187+
List<Pair<String, String>> keyValuePairs = new ArrayList<>();
188+
Pattern pattern = Pattern.compile("\\{%([^:]+):([^%]+)%}");
189+
Matcher matcher = pattern.matcher(input);
190+
191+
while (matcher.find()) {
192+
keyValuePairs.add(new Pair<>(matcher.group(1), matcher.group(2)));
193+
}
194+
195+
return keyValuePairs;
196+
}
197+
198+
protected static int messageTokenCount(HttpMessage msg, List<Pair<String, String>> kvPairs) {
199+
int count = 0;
200+
Collection<SessionToken> tokens = AuthUtils.getAllTokens(msg, false).values();
201+
202+
for (Pair<String, String> kvPair : kvPairs) {
203+
for (SessionToken token : tokens) {
204+
if (token.getSource().equals(kvPair.first)
205+
&& token.getKey().equals(kvPair.second)) {
206+
count++;
207+
break;
208+
}
209+
}
210+
}
211+
return count;
212+
}
213+
214+
@Getter
215+
class AuthRequestDetails {
216+
private HttpMessage msg;
217+
private List<SessionToken> tokens;
218+
private boolean incAllTokens;
219+
private boolean incUsername;
220+
private boolean incPassword;
221+
222+
public AuthRequestDetails(HttpMessage msg) {
223+
this.msg = msg;
224+
String body = msg.getResponseBody().toString();
225+
incUsername =
226+
authCreds != null
227+
&& StringUtils.isNotBlank(authCreds.getUsername())
228+
&& body.contains(authCreds.getUsername());
229+
incPassword =
230+
authCreds != null
231+
&& StringUtils.isNotBlank(authCreds.getPassword())
232+
&& body.contains(authCreds.getPassword());
233+
}
234+
235+
/**
236+
* Is this a better candidate for the authentication request than the supplied
237+
* AuthRequestDetails.
238+
*
239+
* @param ard the details to compare with
240+
* @param headerConfigs - cannot cache these as they may change when session management
241+
* auto-detect used
242+
* @return true if this is a better candidate than the supplied one.
243+
*/
244+
public boolean isBetterThan(
245+
AuthRequestDetails ard, List<Pair<String, String>> headerConfigs) {
246+
int statusCode = msg.getResponseHeader().getStatusCode();
247+
if (HttpStatusCode.isClientError(statusCode)
248+
|| HttpStatusCode.isServerError(statusCode)) {
249+
// Ignore all error responses
250+
return false;
251+
}
252+
if (ard == null) {
253+
return true;
254+
}
255+
// Including the right tokens is the most important thing, assuming there are any
256+
// relevant ones
257+
if (headerConfigs != null) {
258+
// matching any relevant session tokens is the most important thing
259+
List<Pair<String, String>> kvPairs = new ArrayList<>();
260+
for (Pair<String, String> pair : headerConfigs) {
261+
if (HttpHeader.COOKIE.equalsIgnoreCase(pair.first)) {
262+
// We track cookies directly
263+
continue;
264+
}
265+
// Most of the time we'd just expect one token, but we need to cope with an
266+
// arbitrary number
267+
kvPairs.addAll(extractKeyValuePairs(pair.second));
268+
}
269+
if (messageTokenCount(msg, kvPairs) > messageTokenCount(ard.getMsg(), kvPairs)) {
270+
return true;
271+
}
272+
}
273+
if (this.incPassword && !ard.incPassword) {
274+
return true;
275+
}
276+
if (this.incUsername && !ard.incUsername) {
277+
return true;
278+
}
279+
if (isPost(msg) && !isPost(ard.getMsg())) {
280+
return true;
281+
}
282+
// Default to the current one so we always choose the oldest most relevant request
283+
return false;
284+
}
285+
}
144286
}

0 commit comments

Comments
 (0)