Skip to content

Commit 6858acc

Browse files
committed
port experimental cookie models to non-experimental
1 parent 26a24a3 commit 6858acc

File tree

8 files changed

+182
-200
lines changed

8 files changed

+182
-200
lines changed

javascript/ql/lib/semmle/javascript/frameworks/CookieLibraries.qll

Lines changed: 176 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

javascript/ql/src/experimental/Security/CWE-1004/CookieWithoutHttpOnly.ql renamed to javascript/ql/src/Security/CWE-1004/CookieWithoutHttpOnly.ql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
*/
1414

1515
import javascript
16-
import experimental.semmle.javascript.security.InsecureCookie::Cookie as ExperimentalCookie
16+
import experimental.semmle.javascript.security.InsecureCookie::Cookie as ExperimentalCookie // TODO: Remove.
1717

1818
from DataFlow::Node node
1919
where
20+
// TODO: Only for sensitive cookies? (e.g. auth cookies)
21+
// TODO: Give all descriptions, qlhelp, qldocs, an overhaul. Consider precisions, severity, cwes.
2022
exists(ExperimentalCookie::CookieWrite cookie | cookie = node |
2123
cookie.isSensitive() and not cookie.isHttpOnly()
2224
)

javascript/ql/src/experimental/Security/CWE-614/InsecureCookie.ql renamed to javascript/ql/src/Security/CWE-614/InsecureCookie.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import javascript
14-
import experimental.semmle.javascript.security.InsecureCookie::Cookie as ExperimentalCookie
14+
import experimental.semmle.javascript.security.InsecureCookie::Cookie as ExperimentalCookie // TODO: Remove
1515

1616
from DataFlow::Node node
1717
where

0 commit comments

Comments
 (0)