@@ -24,9 +24,20 @@ module CookieWrites {
2424
2525 /**
2626 * Holds if the cookie is likely an authentication cookie or otherwise sensitive.
27+ * Can never hold for client-side cookies. TODO: Or can it...?
2728 */
2829 abstract predicate isSensitive ( ) ;
2930 }
31+
32+ /**
33+ * The flag that indicates that a cookie is secure.
34+ */
35+ string secure ( ) { result = "secure" }
36+
37+ /**
38+ * The flag that indicates that a cookie is HttpOnly.
39+ */
40+ string httpOnly ( ) { result = "httpOnly" }
3041}
3142
3243/**
@@ -50,13 +61,21 @@ private module JsCookie {
5061 }
5162 }
5263
53- class WriteAccess extends PersistentWriteAccess , DataFlow:: CallNode {
54- // TODO: CookieWrite
64+ class WriteAccess extends PersistentWriteAccess , DataFlow:: CallNode , CookieWrites:: CookieWrite {
5565 WriteAccess ( ) { this = libMemberCall ( "set" ) }
5666
5767 string getKey ( ) { getArgument ( 0 ) .mayHaveStringValue ( result ) }
5868
5969 override DataFlow:: Node getValue ( ) { result = getArgument ( 1 ) }
70+
71+ override predicate isSecure ( ) {
72+ // A cookie is secure if there are cookie options with the `secure` flag set to `true`.
73+ this .getOptionArgument ( 2 , CookieWrites:: secure ( ) ) .mayHaveBooleanValue ( true )
74+ }
75+
76+ override predicate isSensitive ( ) { none ( ) } // TODO: Maybe it can be sensitive?
77+
78+ override predicate isHttpOnly ( ) { none ( ) } // js-cookie is browser side library and doesn't support HttpOnly
6079 }
6180}
6281
@@ -117,3 +136,158 @@ private module LibCookie {
117136 override DataFlow:: Node getValue ( ) { result = getArgument ( 1 ) }
118137 }
119138}
139+
140+ /**
141+ * A model of cookies in an express application.
142+ */
143+ private module ExpressCookies {
144+ /**
145+ * A cookie set using `response.cookie` from `express` module (https://expressjs.com/en/api.html#res.cookie).
146+ */
147+ private class InsecureExpressCookieResponse extends CookieWrites:: CookieWrite ,
148+ DataFlow:: MethodCallNode {
149+ InsecureExpressCookieResponse ( ) { this .asExpr ( ) instanceof Express:: SetCookie }
150+
151+ override predicate isSecure ( ) {
152+ // A cookie is secure if there are cookie options with the `secure` flag set to `true`.
153+ // The default is `false`.
154+ this .getOptionArgument ( 2 , CookieWrites:: secure ( ) ) .mayHaveBooleanValue ( true )
155+ }
156+
157+ override predicate isSensitive ( ) {
158+ HeuristicNames:: nameIndicatesSensitiveData ( any ( string s |
159+ this .getArgument ( 0 ) .mayHaveStringValue ( s )
160+ ) , _)
161+ or
162+ this .getArgument ( 0 ) .asExpr ( ) instanceof SensitiveExpr
163+ }
164+
165+ override predicate isHttpOnly ( ) {
166+ // A cookie is httpOnly if there are cookie options with the `httpOnly` flag set to `true`.
167+ // The default is `false`.
168+ this .getOptionArgument ( 2 , CookieWrites:: httpOnly ( ) ) .mayHaveBooleanValue ( true )
169+ }
170+ }
171+
172+ /**
173+ * A cookie set using the `express` module `cookie-session` (https://github.com/expressjs/cookie-session).
174+ */
175+ class InsecureCookieSession extends ExpressLibraries:: CookieSession:: MiddlewareInstance ,
176+ CookieWrites:: CookieWrite {
177+ private DataFlow:: Node getCookieFlagValue ( string flag ) {
178+ result = this .getOptionArgument ( 0 , flag )
179+ }
180+
181+ override predicate isSecure ( ) {
182+ // The flag `secure` is set to `false` by default for HTTP, `true` by default for HTTPS (https://github.com/expressjs/cookie-session#cookie-options).
183+ // A cookie is secure if the `secure` flag is not explicitly set to `false`.
184+ not getCookieFlagValue ( CookieWrites:: secure ( ) ) .mayHaveBooleanValue ( false )
185+ }
186+
187+ override predicate isSensitive ( ) {
188+ any ( ) // It is a session cookie, likely auth sensitive
189+ }
190+
191+ override predicate isHttpOnly ( ) {
192+ // The flag `httpOnly` is set to `true` by default (https://github.com/expressjs/cookie-session#cookie-options).
193+ // A cookie is httpOnly if the `httpOnly` flag is not explicitly set to `false`.
194+ not getCookieFlagValue ( CookieWrites:: httpOnly ( ) ) .mayHaveBooleanValue ( false )
195+ }
196+ }
197+
198+ /**
199+ * A cookie set using the `express` module `express-session` (https://github.com/expressjs/session).
200+ */
201+ class InsecureExpressSessionCookie extends ExpressLibraries:: ExpressSession:: MiddlewareInstance ,
202+ CookieWrites:: CookieWrite {
203+ private DataFlow:: Node getCookieFlagValue ( string flag ) {
204+ result = this .getOption ( "cookie" ) .getALocalSource ( ) .getAPropertyWrite ( flag ) .getRhs ( )
205+ }
206+
207+ override predicate isSecure ( ) {
208+ // The flag `secure` is not set by default (https://github.com/expressjs/session#Cookiesecure).
209+ // The default value for cookie options is { path: '/', httpOnly: true, secure: false, maxAge: null }.
210+ // A cookie is secure if there are the cookie options with the `secure` flag set to `true` or to `auto`.
211+ getCookieFlagValue ( CookieWrites:: secure ( ) ) .mayHaveBooleanValue ( true ) or
212+ getCookieFlagValue ( CookieWrites:: secure ( ) ) .mayHaveStringValue ( "auto" )
213+ }
214+
215+ override predicate isSensitive ( ) {
216+ any ( ) // It is a session cookie, likely auth sensitive
217+ }
218+
219+ override predicate isHttpOnly ( ) {
220+ // The flag `httpOnly` is set by default (https://github.com/expressjs/session#Cookiesecure).
221+ // The default value for cookie options is { path: '/', httpOnly: true, secure: false, maxAge: null }.
222+ // A cookie is httpOnly if the `httpOnly` flag is not explicitly set to `false`.
223+ not getCookieFlagValue ( CookieWrites:: httpOnly ( ) ) .mayHaveBooleanValue ( false )
224+ }
225+ }
226+ }
227+
228+ /**
229+ * A cookie set using `Set-Cookie` header of an `HTTP` response, where a raw header is used.
230+ * (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).
231+ * This class does not model the Express implementation of `HTTP::CookieDefintion`
232+ * as the express implementation does not use raw headers.
233+ *
234+ * In case an array is passed `setHeader("Set-Cookie", [...]` it sets multiple cookies.
235+ * We model a `CookieWrite` for each array element.
236+ */
237+ private class HTTPCookieWrite extends CookieWrites:: CookieWrite {
238+ string header ;
239+
240+ HTTPCookieWrite ( ) {
241+ exists ( HTTP:: CookieDefinition setCookie |
242+ this .asExpr ( ) = setCookie .getHeaderArgument ( ) and
243+ not this instanceof DataFlow:: ArrayCreationNode
244+ or
245+ this = setCookie .getHeaderArgument ( ) .flow ( ) .( DataFlow:: ArrayCreationNode ) .getAnElement ( )
246+ ) and
247+ header =
248+ [
249+ any ( string s | this .mayHaveStringValue ( s ) ) ,
250+ this .( StringOps:: ConcatenationRoot ) .getConstantStringParts ( )
251+ ]
252+ }
253+
254+ override predicate isSecure ( ) {
255+ // A cookie is secure if the `secure` flag is specified in the cookie definition.
256+ // The default is `false`.
257+ hasCookieAttribute ( header , CookieWrites:: secure ( ) )
258+ }
259+
260+ override predicate isHttpOnly ( ) {
261+ // A cookie is httpOnly if the `httpOnly` flag is specified in the cookie definition.
262+ // The default is `false`.
263+ hasCookieAttribute ( header , CookieWrites:: httpOnly ( ) )
264+ }
265+
266+ override predicate isSensitive ( ) {
267+ HeuristicNames:: nameIndicatesSensitiveData ( getCookieName ( header ) , _)
268+ }
269+
270+ /**
271+ * Gets cookie name from a `Set-Cookie` header value.
272+ * The header value always starts with `<cookie-name>=<cookie-value>` optionally followed by attributes:
273+ * `<cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly`
274+ */
275+ bindingset [ s]
276+ private string getCookieName ( string s ) {
277+ result = s .regexpCapture ( "\\s*\\b([^=\\s]*)\\b\\s*=.*" , 1 )
278+ }
279+
280+ /**
281+ * Holds if the `Set-Cookie` header value contains the specified attribute
282+ * 1. The attribute is case insensitive
283+ * 2. It always starts with a pair `<cookie-name>=<cookie-value>`.
284+ * If the attribute is present there must be `;` after the pair.
285+ * Other attributes like `Domain=`, `Path=`, etc. may come after the pair:
286+ * `<cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly`
287+ * See `https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie`
288+ */
289+ bindingset [ s, attribute]
290+ private predicate hasCookieAttribute ( string s , string attribute ) {
291+ s .regexpMatch ( "(?i).*;\\s*" + attribute + "\\b\\s*;?.*$" )
292+ }
293+ }
0 commit comments