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,209 @@ 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
+ && 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
+ }
72
112
}
113
+ AuthRequestDetails candidate = new AuthRequestDetails (msg );
73
114
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 ();
77
121
}
78
122
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 ;
82
129
}
130
+
83
131
Set <SessionToken > reqSessionTokens = AuthUtils .getRequestSessionTokens (msg );
132
+ Set <SessionToken > unkSessionTokens = new HashSet <>();
84
133
for (SessionToken token : reqSessionTokens ) {
85
134
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 );
87
140
}
88
141
}
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 ;
111
155
}
112
156
}
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
157
}
124
158
}
125
159
126
- private String getHrefId (HttpMessage msg ) {
127
- if (msg .getHistoryRef () != null ) {
128
- return Integer .toString (msg .getHistoryRef ().getHistoryId ());
129
- }
130
- return "?" ;
131
- }
132
-
133
160
public HttpMessage getAuthMsg () {
134
- return authMsg ;
161
+ return authReq . getMsg () ;
135
162
}
136
163
137
164
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 ();
139
180
}
140
181
141
182
public HttpMessage getFallbackMsg () {
142
183
return fallbackMsg ;
143
184
}
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
+ }
144
286
}
0 commit comments