4
4
5
5
import javascript
6
6
7
+ /**
8
+ * Classes and predicates for reasoning about writes to cookies.
9
+ */
10
+ module CookieWrites {
11
+ /**
12
+ * A write to a cookie.
13
+ */
14
+ abstract class CookieWrite extends DataFlow:: Node {
15
+ /**
16
+ * Holds if this cookie is secure, i.e. only transmitted over SSL.
17
+ */
18
+ abstract predicate isSecure ( ) ;
19
+
20
+ /**
21
+ * Holds if this cookie is HttpOnly, i.e. not accessible by JavaScript.
22
+ */
23
+ abstract predicate isHttpOnly ( ) ;
24
+
25
+ /**
26
+ * Holds if the cookie likely is an authentication cookie or otherwise sensitive.
27
+ */
28
+ abstract predicate isSensitive ( ) ;
29
+
30
+ /**
31
+ * Holds if the cookie write happens on a server, i.e. the `httpOnly` flag is relevant.
32
+ */
33
+ predicate isServerSide ( ) {
34
+ any ( ) // holds by default. Client-side cookie writes should extend ClientSideCookieWrite.
35
+ }
36
+ }
37
+
38
+ /**
39
+ * A client-side write to a cookie.
40
+ */
41
+ abstract class ClientSideCookieWrite extends CookieWrite {
42
+ final override predicate isHttpOnly ( ) { none ( ) }
43
+
44
+ final override predicate isServerSide ( ) { none ( ) }
45
+ }
46
+
47
+ /**
48
+ * The flag that indicates that a cookie is secure.
49
+ */
50
+ string secure ( ) { result = "secure" }
51
+
52
+ /**
53
+ * The flag that indicates that a cookie is HttpOnly.
54
+ */
55
+ string httpOnly ( ) { result = "httpOnly" }
56
+ }
57
+
58
+ /**
59
+ * Holds if `node` looks like it can contain a sensitive cookie.
60
+ *
61
+ * Heuristics:
62
+ * - `node` contains a string value that looks like a sensitive cookie name
63
+ * - `node` is a sensitive expression
64
+ */
65
+ private predicate canHaveSensitiveCookie ( DataFlow:: Node node ) {
66
+ exists ( string s |
67
+ node .mayHaveStringValue ( s ) or
68
+ s = node .( StringOps:: ConcatenationRoot ) .getConstantStringParts ( )
69
+ |
70
+ HeuristicNames:: nameIndicatesSensitiveData ( [ s , getCookieName ( s ) ] , _)
71
+ )
72
+ or
73
+ node .asExpr ( ) instanceof SensitiveExpr
74
+ }
75
+
76
+ /**
77
+ * Gets the cookie name of a `Set-Cookie` header value.
78
+ * The header value always starts with `<cookie-name>=<cookie-value>` optionally followed by attributes:
79
+ * `<cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly`
80
+ */
81
+ bindingset [ s]
82
+ private string getCookieName ( string s ) { result = s .regexpCapture ( "([^=]*)=.*" , 1 ) .trim ( ) }
83
+
84
+ /**
85
+ * Holds if the `Set-Cookie` header value contains the specified attribute
86
+ * 1. The attribute is case insensitive
87
+ * 2. It always starts with a pair `<cookie-name>=<cookie-value>`.
88
+ * If the attribute is present there must be `;` after the pair.
89
+ * Other attributes like `Domain=`, `Path=`, etc. may come after the pair:
90
+ * `<cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly`
91
+ * See `https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie`
92
+ */
93
+ bindingset [ s, attribute]
94
+ private predicate hasCookieAttribute ( string s , string attribute ) {
95
+ s .regexpMatch ( "(?i).*;\\s*" + attribute + "\\b\\s*;?.*$" )
96
+ }
97
+
7
98
/**
8
99
* A model of the `js-cookie` library (https://github.com/js-cookie/js-cookie).
9
100
*/
@@ -25,12 +116,22 @@ private module JsCookie {
25
116
}
26
117
}
27
118
28
- class WriteAccess extends PersistentWriteAccess , DataFlow:: CallNode {
119
+ class WriteAccess extends PersistentWriteAccess , DataFlow:: CallNode ,
120
+ CookieWrites:: ClientSideCookieWrite {
29
121
WriteAccess ( ) { this = libMemberCall ( "set" ) }
30
122
31
123
string getKey ( ) { getArgument ( 0 ) .mayHaveStringValue ( result ) }
32
124
33
125
override DataFlow:: Node getValue ( ) { result = getArgument ( 1 ) }
126
+
127
+ override predicate isSecure ( ) {
128
+ // A cookie is secure if there are cookie options with the `secure` flag set to `true`.
129
+ exists ( DataFlow:: Node value | value = this .getOptionArgument ( 2 , CookieWrites:: secure ( ) ) |
130
+ not value .mayHaveBooleanValue ( false ) // anything but `false` is accepted as being maybe true
131
+ )
132
+ }
133
+
134
+ override predicate isSensitive ( ) { canHaveSensitiveCookie ( this .getArgument ( 0 ) ) }
34
135
}
35
136
}
36
137
@@ -53,12 +154,25 @@ private module BrowserCookies {
53
154
}
54
155
}
55
156
56
- class WriteAccess extends PersistentWriteAccess , DataFlow:: CallNode {
157
+ class WriteAccess extends PersistentWriteAccess , DataFlow:: CallNode ,
158
+ CookieWrites:: ClientSideCookieWrite {
57
159
WriteAccess ( ) { this = libMemberCall ( "set" ) }
58
160
59
161
string getKey ( ) { getArgument ( 0 ) .mayHaveStringValue ( result ) }
60
162
61
163
override DataFlow:: Node getValue ( ) { result = getArgument ( 1 ) }
164
+
165
+ override predicate isSecure ( ) {
166
+ // A cookie is secure if there are cookie options with the `secure` flag set to `true`.
167
+ exists ( DataFlow:: Node value | value = this .getOptionArgument ( 2 , CookieWrites:: secure ( ) ) |
168
+ not value .mayHaveBooleanValue ( false ) // anything but `false` is accepted as being maybe true
169
+ )
170
+ or
171
+ // or, an explicit default has been set
172
+ exists ( DataFlow:: moduleMember ( "browser-cookies" , "defaults" ) .getAPropertyWrite ( "secure" ) )
173
+ }
174
+
175
+ override predicate isSensitive ( ) { canHaveSensitiveCookie ( this .getArgument ( 0 ) ) }
62
176
}
63
177
}
64
178
@@ -81,11 +195,174 @@ private module LibCookie {
81
195
override PersistentWriteAccess getAWrite ( ) { key = result .( WriteAccess ) .getKey ( ) }
82
196
}
83
197
84
- class WriteAccess extends PersistentWriteAccess , DataFlow:: CallNode {
198
+ class WriteAccess extends PersistentWriteAccess , DataFlow:: CallNode ,
199
+ CookieWrites:: ClientSideCookieWrite {
85
200
WriteAccess ( ) { this = libMemberCall ( "serialize" ) }
86
201
87
202
string getKey ( ) { getArgument ( 0 ) .mayHaveStringValue ( result ) }
88
203
89
204
override DataFlow:: Node getValue ( ) { result = getArgument ( 1 ) }
205
+
206
+ override predicate isSecure ( ) {
207
+ // A cookie is secure if there are cookie options with the `secure` flag set to `true`.
208
+ exists ( DataFlow:: Node value | value = this .getOptionArgument ( 2 , CookieWrites:: secure ( ) ) |
209
+ not value .mayHaveBooleanValue ( false ) // anything but `false` is accepted as being maybe true
210
+ )
211
+ }
212
+
213
+ override predicate isSensitive ( ) { canHaveSensitiveCookie ( this .getArgument ( 0 ) ) }
214
+ }
215
+ }
216
+
217
+ /**
218
+ * A model of cookies in an express application.
219
+ */
220
+ private module ExpressCookies {
221
+ /**
222
+ * A cookie set using `response.cookie` from `express` module (https://expressjs.com/en/api.html#res.cookie).
223
+ */
224
+ private class InsecureExpressCookieResponse extends CookieWrites:: CookieWrite ,
225
+ DataFlow:: MethodCallNode {
226
+ InsecureExpressCookieResponse ( ) { this .asExpr ( ) instanceof Express:: SetCookie }
227
+
228
+ override predicate isSecure ( ) {
229
+ // A cookie is secure if there are cookie options with the `secure` flag set to `true`.
230
+ // The default is `false`.
231
+ exists ( DataFlow:: Node value | value = this .getOptionArgument ( 2 , CookieWrites:: secure ( ) ) |
232
+ not value .mayHaveBooleanValue ( false ) // anything but `false` is accepted as being maybe true
233
+ )
234
+ }
235
+
236
+ override predicate isSensitive ( ) { canHaveSensitiveCookie ( this .getArgument ( 0 ) ) }
237
+
238
+ override predicate isHttpOnly ( ) {
239
+ // A cookie is httpOnly if there are cookie options with the `httpOnly` flag set to `true`.
240
+ // The default is `false`.
241
+ exists ( DataFlow:: Node value | value = this .getOptionArgument ( 2 , CookieWrites:: httpOnly ( ) ) |
242
+ not value .mayHaveBooleanValue ( false ) // anything but `false` is accepted as being maybe true
243
+ )
244
+ }
245
+ }
246
+
247
+ /**
248
+ * A cookie set using the `express` module `cookie-session` (https://github.com/expressjs/cookie-session).
249
+ */
250
+ class InsecureCookieSession extends ExpressLibraries:: CookieSession:: MiddlewareInstance ,
251
+ CookieWrites:: CookieWrite {
252
+ private DataFlow:: Node getCookieFlagValue ( string flag ) {
253
+ result = this .getOptionArgument ( 0 , flag )
254
+ }
255
+
256
+ override predicate isSecure ( ) {
257
+ // The flag `secure` is set to `false` by default for HTTP, `true` by default for HTTPS (https://github.com/expressjs/cookie-session#cookie-options).
258
+ // A cookie is secure if the `secure` flag is not explicitly set to `false`.
259
+ not getCookieFlagValue ( CookieWrites:: secure ( ) ) .mayHaveBooleanValue ( false )
260
+ }
261
+
262
+ override predicate isSensitive ( ) {
263
+ any ( ) // It is a session cookie, likely auth sensitive
264
+ }
265
+
266
+ override predicate isHttpOnly ( ) {
267
+ // The flag `httpOnly` is set to `true` by default (https://github.com/expressjs/cookie-session#cookie-options).
268
+ // A cookie is httpOnly if the `httpOnly` flag is not explicitly set to `false`.
269
+ not getCookieFlagValue ( CookieWrites:: httpOnly ( ) ) .mayHaveBooleanValue ( false )
270
+ }
271
+ }
272
+
273
+ /**
274
+ * A cookie set using the `express` module `express-session` (https://github.com/expressjs/session).
275
+ */
276
+ class InsecureExpressSessionCookie extends ExpressLibraries:: ExpressSession:: MiddlewareInstance ,
277
+ CookieWrites:: CookieWrite {
278
+ private DataFlow:: Node getCookieFlagValue ( string flag ) {
279
+ result = this .getOption ( "cookie" ) .getALocalSource ( ) .getAPropertyWrite ( flag ) .getRhs ( )
280
+ }
281
+
282
+ override predicate isSecure ( ) {
283
+ // The flag `secure` is not set by default (https://github.com/expressjs/session#Cookiesecure).
284
+ // The default value for cookie options is { path: '/', httpOnly: true, secure: false, maxAge: null }.
285
+ exists ( DataFlow:: Node value | value = getCookieFlagValue ( CookieWrites:: secure ( ) ) |
286
+ not value .mayHaveBooleanValue ( false ) // anything but `false` is accepted as being maybe true
287
+ )
288
+ }
289
+
290
+ override predicate isSensitive ( ) {
291
+ any ( ) // It is a session cookie, likely auth sensitive
292
+ }
293
+
294
+ override predicate isHttpOnly ( ) {
295
+ // The flag `httpOnly` is set by default (https://github.com/expressjs/session#Cookiesecure).
296
+ // The default value for cookie options is { path: '/', httpOnly: true, secure: false, maxAge: null }.
297
+ // A cookie is httpOnly if the `httpOnly` flag is not explicitly set to `false`.
298
+ not getCookieFlagValue ( CookieWrites:: httpOnly ( ) ) .mayHaveBooleanValue ( false )
299
+ }
300
+ }
301
+ }
302
+
303
+ /**
304
+ * A cookie set using `Set-Cookie` header of an `HTTP` response, where a raw header is used.
305
+ * (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).
306
+ * This class does not model the Express implementation of `HTTP::CookieDefintion`
307
+ * as the express implementation does not use raw headers.
308
+ *
309
+ * In case an array is passed `setHeader("Set-Cookie", [...]` it sets multiple cookies.
310
+ * We model a `CookieWrite` for each array element.
311
+ */
312
+ private class HTTPCookieWrite extends CookieWrites:: CookieWrite {
313
+ string header ;
314
+
315
+ HTTPCookieWrite ( ) {
316
+ exists ( HTTP:: CookieDefinition setCookie |
317
+ this .asExpr ( ) = setCookie .getHeaderArgument ( ) and
318
+ not this instanceof DataFlow:: ArrayCreationNode
319
+ or
320
+ this = setCookie .getHeaderArgument ( ) .flow ( ) .( DataFlow:: ArrayCreationNode ) .getAnElement ( )
321
+ ) and
322
+ header =
323
+ [
324
+ any ( string s | this .mayHaveStringValue ( s ) ) ,
325
+ this .( StringOps:: ConcatenationRoot ) .getConstantStringParts ( )
326
+ ]
327
+ }
328
+
329
+ override predicate isSecure ( ) {
330
+ // A cookie is secure if the `secure` flag is specified in the cookie definition.
331
+ // The default is `false`.
332
+ hasCookieAttribute ( header , CookieWrites:: secure ( ) )
90
333
}
334
+
335
+ override predicate isHttpOnly ( ) {
336
+ // A cookie is httpOnly if the `httpOnly` flag is specified in the cookie definition.
337
+ // The default is `false`.
338
+ hasCookieAttribute ( header , CookieWrites:: httpOnly ( ) )
339
+ }
340
+
341
+ override predicate isSensitive ( ) { canHaveSensitiveCookie ( this ) }
342
+ }
343
+
344
+ /**
345
+ * A write to `document.cookie`.
346
+ */
347
+ private class DocumentCookieWrite extends CookieWrites:: ClientSideCookieWrite {
348
+ string cookie ;
349
+ DataFlow:: PropWrite write ;
350
+
351
+ DocumentCookieWrite ( ) {
352
+ this = write and
353
+ write = DOM:: documentRef ( ) .getAPropertyWrite ( "cookie" ) and
354
+ cookie =
355
+ [
356
+ any ( string s | write .getRhs ( ) .mayHaveStringValue ( s ) ) ,
357
+ write .getRhs ( ) .( StringOps:: ConcatenationRoot ) .getConstantStringParts ( )
358
+ ]
359
+ }
360
+
361
+ override predicate isSecure ( ) {
362
+ // A cookie is secure if the `secure` flag is specified in the cookie definition.
363
+ // The default is `false`.
364
+ hasCookieAttribute ( cookie , CookieWrites:: secure ( ) )
365
+ }
366
+
367
+ override predicate isSensitive ( ) { canHaveSensitiveCookie ( write .getRhs ( ) ) }
91
368
}
0 commit comments