20
20
package org .zaproxy .addon .authhelper .internal ;
21
21
22
22
import java .util .ArrayList ;
23
- import java .util .Map ;
23
+ import java .util .Collection ;
24
+ import java .util .HashSet ;
25
+ import java .util .List ;
24
26
import java .util .Set ;
27
+ import java .util .regex .Matcher ;
28
+ import java .util .regex .Pattern ;
29
+ import lombok .Getter ;
25
30
import lombok .Setter ;
26
- import org .apache .logging .log4j .Level ;
31
+ import org .apache .commons .httpclient .URIException ;
32
+ import org .apache .commons .lang3 .StringUtils ;
27
33
import org .apache .logging .log4j .LogManager ;
28
34
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 ;
31
36
import org .parosproxy .paros .network .HttpMessage ;
32
37
import org .parosproxy .paros .network .HttpRequestHeader ;
33
- import org .parosproxy .paros .view . View ;
38
+ import org .parosproxy .paros .network . HttpStatusCode ;
34
39
import org .zaproxy .addon .authhelper .AuthUtils ;
40
+ import org .zaproxy .addon .authhelper .HeaderBasedSessionManagementMethodType ;
35
41
import org .zaproxy .addon .authhelper .HistoryProvider ;
36
42
import org .zaproxy .addon .authhelper .SessionManagementRequestDetails ;
37
43
import org .zaproxy .addon .authhelper .SessionToken ;
38
44
import org .zaproxy .addon .network .server .HttpMessageHandler ;
39
45
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 ;
41
50
42
51
public final class ClientSideHandler implements HttpMessageHandler {
43
52
44
53
private static final Logger LOGGER = LogManager .getLogger (ClientSideHandler .class );
45
54
46
- private final Context context ;
47
- private HttpMessage authMsg ;
55
+ private final User user ;
56
+ private UsernamePasswordAuthenticationCredentials authCreds ;
57
+ private AuthRequestDetails authReq ;
48
58
private HttpMessage fallbackMsg ;
49
59
private int firstHrefId ;
50
60
51
61
@ Setter private HistoryProvider historyProvider = new HistoryProvider ();
52
62
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
+ }
55
69
}
56
70
57
71
private boolean isPost (HttpMessage msg ) {
@@ -64,81 +78,210 @@ public void handleMessage(HttpMessageHandlerContext ctx, HttpMessage msg) {
64
78
if (ctx .isFromClient ()) {
65
79
return ;
66
80
}
81
+ if (firstHrefId == 0 && msg .getHistoryRef () != null ) {
82
+ // Backstop for looping back through the history
83
+ firstHrefId = msg .getHistoryRef ().getHistoryId ();
84
+ }
67
85
68
86
historyProvider .addAuthMessageToHistory (msg );
69
87
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
+ }
72
113
}
114
+ AuthRequestDetails candidate = new AuthRequestDetails (msg );
73
115
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 ();
77
122
}
78
123
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 ;
82
130
}
131
+
83
132
Set <SessionToken > reqSessionTokens = AuthUtils .getRequestSessionTokens (msg );
133
+ Set <SessionToken > unkSessionTokens = new HashSet <>();
84
134
for (SessionToken token : reqSessionTokens ) {
85
135
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 );
87
141
}
88
142
}
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 ;
111
156
}
112
157
}
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 ();
123
158
}
124
159
}
125
160
126
- private String getHrefId (HttpMessage msg ) {
127
- if (msg .getHistoryRef () != null ) {
128
- return Integer .toString (msg .getHistoryRef ().getHistoryId ());
129
- }
130
- return "?" ;
131
- }
132
-
133
161
public HttpMessage getAuthMsg () {
134
- return authMsg ;
162
+ return authReq . getMsg () ;
135
163
}
136
164
137
165
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 ();
139
181
}
140
182
141
183
public HttpMessage getFallbackMsg () {
142
184
return fallbackMsg ;
143
185
}
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
+ }
144
287
}
0 commit comments