Skip to content

Commit 521f853

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

16 files changed

+1229
-86
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

+203-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,210 @@ 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+
&& user.getAuthenticationCredentials()
92+
instanceof UsernamePasswordAuthenticationCredentials upCreds
93+
&& StringUtils.isNotEmpty(upCreds.getUsername())
94+
&& StringUtils.isNotEmpty(upCreds.getPassword())
95+
&& reqBody.contains(upCreds.getUsername())
96+
&& reqBody.contains(upCreds.getPassword())
97+
&& AuthUtils.getSessionManagementDetailsForContext(user.getContext().getId())
98+
!= null) {
99+
// The app is sending user creds to another site. Assume this is part of the valid
100+
// auth flow and add to the context
101+
try {
102+
user.getContext()
103+
.addIncludeInContextRegex(SessionStructure.getHostName(msg) + ".*");
104+
} catch (URIException e) {
105+
// Very unexpected
106+
LOGGER.error(e.getMessage(), e);
107+
return;
108+
}
109+
} else {
110+
// Not in the context, no creds, not relevant
111+
return;
112+
}
72113
}
114+
AuthRequestDetails candidate = new AuthRequestDetails(msg);
73115

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

79-
if (authMsg != null && isPost(authMsg) && !isPost(msg)) {
80-
// We have a better candidate
81-
return;
124+
if (candidate.isBetterThan(authReq, headerConfigs)) {
125+
LOGGER.debug(
126+
"Found better auth candidate {} {}",
127+
msg.getRequestHeader().getMethod(),
128+
msg.getRequestHeader().getURI());
129+
authReq = candidate;
82130
}
131+
83132
Set<SessionToken> reqSessionTokens = AuthUtils.getRequestSessionTokens(msg);
133+
Set<SessionToken> unkSessionTokens = new HashSet<>();
84134
for (SessionToken token : reqSessionTokens) {
85135
if (!SessionToken.COOKIE_SOURCE.equals(token.getSource())) {
86-
AuthUtils.recordRequestSessionToken(context, token.getKey(), token.getValue());
136+
AuthUtils.recordRequestSessionToken(
137+
user.getContext(), token.getKey(), token.getValue());
138+
}
139+
if (AuthUtils.containsSessionToken(token.getValue()) == null) {
140+
unkSessionTokens.add(token);
87141
}
88142
}
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-
}
143+
for (SessionToken st : unkSessionTokens) {
144+
// See if we can find the reqs for the unknown session tokens, then see if they are
145+
// better than the current one
146+
SessionManagementRequestDetails smReqDetails =
147+
AuthUtils.findSessionTokenSource(st.getValue(), firstHrefId);
148+
if (smReqDetails != null) {
149+
candidate = new AuthRequestDetails(msg);
150+
if (candidate.isBetterThan(authReq, headerConfigs)) {
151+
LOGGER.debug(
152+
"Found better auth candidate {} {}",
153+
msg.getRequestHeader().getMethod(),
154+
msg.getRequestHeader().getURI());
155+
authReq = candidate;
111156
}
112157
}
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();
123158
}
124159
}
125160

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

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

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

0 commit comments

Comments
 (0)