44
55import javascript
66
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+
798/**
899 * A model of the `js-cookie` library (https://github.com/js-cookie/js-cookie).
9100 */
@@ -25,12 +116,22 @@ private module JsCookie {
25116 }
26117 }
27118
28- class WriteAccess extends PersistentWriteAccess , DataFlow:: CallNode {
119+ class WriteAccess extends PersistentWriteAccess , DataFlow:: CallNode ,
120+ CookieWrites:: ClientSideCookieWrite {
29121 WriteAccess ( ) { this = libMemberCall ( "set" ) }
30122
31123 string getKey ( ) { getArgument ( 0 ) .mayHaveStringValue ( result ) }
32124
33125 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 ) ) }
34135 }
35136}
36137
@@ -53,12 +154,25 @@ private module BrowserCookies {
53154 }
54155 }
55156
56- class WriteAccess extends PersistentWriteAccess , DataFlow:: CallNode {
157+ class WriteAccess extends PersistentWriteAccess , DataFlow:: CallNode ,
158+ CookieWrites:: ClientSideCookieWrite {
57159 WriteAccess ( ) { this = libMemberCall ( "set" ) }
58160
59161 string getKey ( ) { getArgument ( 0 ) .mayHaveStringValue ( result ) }
60162
61163 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 ) ) }
62176 }
63177}
64178
@@ -81,11 +195,174 @@ private module LibCookie {
81195 override PersistentWriteAccess getAWrite ( ) { key = result .( WriteAccess ) .getKey ( ) }
82196 }
83197
84- class WriteAccess extends PersistentWriteAccess , DataFlow:: CallNode {
198+ class WriteAccess extends PersistentWriteAccess , DataFlow:: CallNode ,
199+ CookieWrites:: ClientSideCookieWrite {
85200 WriteAccess ( ) { this = libMemberCall ( "serialize" ) }
86201
87202 string getKey ( ) { getArgument ( 0 ) .mayHaveStringValue ( result ) }
88203
89204 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 ( ) )
90333 }
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 ( ) ) }
91368}
0 commit comments