Skip to content

Commit 44db920

Browse files
committed
refactor, cleanup, and improvements in experimental cookie queries
1 parent 2b9edd7 commit 44db920

File tree

7 files changed

+88
-173
lines changed

7 files changed

+88
-173
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@
1515
import javascript
1616
import experimental.semmle.javascript.security.InsecureCookie::Cookie
1717

18-
from Cookie cookie
19-
where cookie.isAuthNotHttpOnly()
18+
from CookieWrite cookie
19+
where cookie.isSensitive() and not cookie.isHttpOnly()
2020
select cookie, "Cookie attribute 'HttpOnly' is not set to true for this sensitive cookie."

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
import javascript
1414
import experimental.semmle.javascript.security.InsecureCookie::Cookie
1515

16-
from Cookie cookie
16+
from CookieWrite cookie
1717
where not cookie.isSecure()
1818
select cookie, "Cookie is added to response without the 'secure' flag being set to true"

javascript/ql/src/experimental/semmle/javascript/security/InsecureCookie.qll

Lines changed: 58 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
*/
66

77
import javascript
8+
private import semmle.javascript.security.SensitiveActions
89

10+
// TODO: Move this entire file into stdlib.
11+
// TODO: make "session", "auth", a sensitive name.
12+
// TODO: Have helper predicate that selects the relevant Sensitive Classifications.
13+
// TODO: Look for more cookie libraries.
914
module Cookie {
1015
/**
1116
* `secure` property of the cookie options.
@@ -18,70 +23,32 @@ module Cookie {
1823
string httpOnlyFlag() { result = "httpOnly" }
1924

2025
/**
21-
* Abstract class to represent different cases of insecure cookie settings.
26+
* A write to a cookie.
2227
*/
23-
abstract class Cookie extends DataFlow::Node {
28+
abstract class CookieWrite extends DataFlow::Node {
2429
/**
25-
* Gets the name of the middleware/library used to set the cookie.
26-
*/
27-
abstract string getKind();
28-
29-
/**
30-
* Gets the options used to set this cookie, if any.
31-
*/
32-
abstract DataFlow::Node getCookieOptionsArgument();
33-
34-
/**
35-
* Holds if this cookie is secure.
30+
* Holds if this cookie is secure, i.e. only transmitted over SSL.
3631
*/
3732
abstract predicate isSecure();
3833

3934
/**
40-
* Holds if this cookie is HttpOnly.
35+
* Holds if this cookie is HttpOnly, i.e. not accessible by JavaScript.
4136
*/
4237
abstract predicate isHttpOnly();
4338

4439
/**
45-
* Holds if the cookie is authentication sensitive and lacks HttpOnly.
40+
* Holds if the cookie is likely an authentication cookie or otherwise sensitive.
4641
*/
47-
abstract predicate isAuthNotHttpOnly();
48-
}
49-
50-
/**
51-
* Holds if the expression is a variable with a sensitive name.
52-
*/
53-
private predicate isAuthVariable(DataFlow::Node expr) {
54-
exists(string val |
55-
(
56-
val = expr.getStringValue() or
57-
val = expr.asExpr().(VarAccess).getName() or
58-
val = expr.(DataFlow::PropRead).getPropertyName()
59-
) and
60-
regexpMatchAuth(val)
61-
)
62-
or
63-
isAuthVariable(expr.getAPredecessor())
64-
}
65-
66-
/**
67-
* Holds if `val` looks related to authentication, without being an anti-forgery token.
68-
*/
69-
bindingset[val]
70-
private predicate regexpMatchAuth(string val) {
71-
val.regexpMatch("(?i).*(session|login|token|user|auth|credential).*") and
72-
not val.regexpMatch("(?i).*(xsrf|csrf|forgery).*")
42+
abstract predicate isSensitive();
7343
}
7444

7545
/**
7646
* A cookie set using the `express` module `cookie-session` (https://github.com/expressjs/cookie-session).
7747
*/
78-
class InsecureCookieSession extends ExpressLibraries::CookieSession::MiddlewareInstance, Cookie {
79-
override string getKind() { result = "cookie-session" }
80-
81-
override DataFlow::SourceNode getCookieOptionsArgument() { result.flowsTo(getArgument(0)) }
82-
48+
class InsecureCookieSession extends ExpressLibraries::CookieSession::MiddlewareInstance,
49+
CookieWrite {
8350
private DataFlow::Node getCookieFlagValue(string flag) {
84-
result = this.getCookieOptionsArgument().getAPropertyWrite(flag).getRhs()
51+
result = this.getOptionArgument(0, flag)
8552
}
8653

8754
override predicate isSecure() {
@@ -90,8 +57,8 @@ module Cookie {
9057
not getCookieFlagValue(secureFlag()).mayHaveBooleanValue(false)
9158
}
9259

93-
override predicate isAuthNotHttpOnly() {
94-
not isHttpOnly() // It is a session cookie, likely auth sensitive
60+
override predicate isSensitive() {
61+
any() // It is a session cookie, likely auth sensitive
9562
}
9663

9764
override predicate isHttpOnly() {
@@ -105,13 +72,9 @@ module Cookie {
10572
* A cookie set using the `express` module `express-session` (https://github.com/expressjs/session).
10673
*/
10774
class InsecureExpressSessionCookie extends ExpressLibraries::ExpressSession::MiddlewareInstance,
108-
Cookie {
109-
override string getKind() { result = "express-session" }
110-
111-
override DataFlow::SourceNode getCookieOptionsArgument() { result = this.getOption("cookie") }
112-
75+
CookieWrite {
11376
private DataFlow::Node getCookieFlagValue(string flag) {
114-
result = this.getCookieOptionsArgument().getAPropertyWrite(flag).getRhs()
77+
result = this.getOption("cookie").getALocalSource().getAPropertyWrite(flag).getRhs()
11578
}
11679

11780
override predicate isSecure() {
@@ -122,8 +85,8 @@ module Cookie {
12285
getCookieFlagValue(secureFlag()).mayHaveStringValue("auto")
12386
}
12487

125-
override predicate isAuthNotHttpOnly() {
126-
not isHttpOnly() // It is a session cookie, likely auth sensitive
88+
override predicate isSensitive() {
89+
any() // It is a session cookie, likely auth sensitive
12790
}
12891

12992
override predicate isHttpOnly() {
@@ -137,17 +100,11 @@ module Cookie {
137100
/**
138101
* A cookie set using `response.cookie` from `express` module (https://expressjs.com/en/api.html#res.cookie).
139102
*/
140-
class InsecureExpressCookieResponse extends Cookie, DataFlow::MethodCallNode {
103+
class InsecureExpressCookieResponse extends CookieWrite, DataFlow::MethodCallNode {
141104
InsecureExpressCookieResponse() { this.calls(any(Express::ResponseExpr r).flow(), "cookie") }
142105

143-
override string getKind() { result = "response.cookie" }
144-
145-
override DataFlow::SourceNode getCookieOptionsArgument() {
146-
result = this.getLastArgument().getALocalSource()
147-
}
148-
149106
private DataFlow::Node getCookieFlagValue(string flag) {
150-
result = this.getCookieOptionsArgument().getAPropertyWrite(flag).getRhs()
107+
result = this.getOptionArgument(this.getNumArgument() - 1, flag)
151108
}
152109

153110
override predicate isSecure() {
@@ -156,9 +113,12 @@ module Cookie {
156113
getCookieFlagValue(secureFlag()).mayHaveBooleanValue(true)
157114
}
158115

159-
override predicate isAuthNotHttpOnly() {
160-
isAuthVariable(this.getArgument(0)) and
161-
not isHttpOnly()
116+
override predicate isSensitive() {
117+
HeuristicNames::nameIndicatesSensitiveData(any(string s |
118+
this.getArgument(0).mayHaveStringValue(s)
119+
), _)
120+
or
121+
this.getArgument(0).asExpr() instanceof SensitiveExpr
162122
}
163123

164124
override predicate isHttpOnly() {
@@ -168,101 +128,56 @@ module Cookie {
168128
}
169129
}
170130

171-
private class AttributeToSetCookieHeaderTrackingConfig extends TaintTracking::Configuration {
172-
AttributeToSetCookieHeaderTrackingConfig() { this = "AttributeToSetCookieHeaderTrackingConfig" }
173-
174-
override predicate isSource(DataFlow::Node source) {
175-
exists(string s | source.mayHaveStringValue(s))
176-
}
177-
178-
override predicate isSink(DataFlow::Node sink) { sink.asExpr() instanceof TemplateLiteral }
179-
}
180-
181-
private class SensitiveNameToSetCookieHeaderTrackingConfig extends TaintTracking::Configuration {
182-
SensitiveNameToSetCookieHeaderTrackingConfig() {
183-
this = "SensitiveNameToSetCookieHeaderTrackingConfig"
184-
}
185-
186-
override predicate isSource(DataFlow::Node source) { isAuthVariable(source) }
187-
188-
override predicate isSink(DataFlow::Node sink) { sink.asExpr() instanceof TemplateLiteral }
189-
}
190-
191131
/**
192132
* A cookie set using `Set-Cookie` header of an `HTTP` response.
193133
* (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).
194134
* In case an array is passed `setHeader("Set-Cookie", [...]` it sets multiple cookies.
195135
* Each array element has its own attributes.
196136
*/
197-
class InsecureSetCookieHeader extends Cookie {
137+
class InsecureSetCookieHeader extends CookieWrite {
198138
InsecureSetCookieHeader() {
199-
this.asExpr() = any(HTTP::SetCookieHeader setCookie).getHeaderArgument()
200-
}
201-
202-
override string getKind() { result = "set-cookie header" }
203-
204-
override DataFlow::Node getCookieOptionsArgument() {
205-
if this.asExpr() instanceof ArrayExpr
206-
then result.asExpr() = this.asExpr().(ArrayExpr).getAnElement()
207-
else result.asExpr() = this.asExpr()
139+
this.asExpr() = any(HTTP::SetCookieHeader setCookie).getHeaderArgument() and
140+
not this instanceof DataFlow::ArrayCreationNode
141+
or
142+
this =
143+
any(HTTP::SetCookieHeader setCookie)
144+
.getHeaderArgument()
145+
.flow()
146+
.(DataFlow::ArrayCreationNode)
147+
.getAnElement()
208148
}
209149

210150
/**
211151
* A cookie is secure if the `secure` flag is specified in the cookie definition.
212152
* The default is `false`.
213153
*/
214-
override predicate isSecure() { allHaveCookieAttribute("secure") }
154+
override predicate isSecure() { hasCookieAttribute("secure") }
215155

216156
/**
217157
* A cookie is httpOnly if the `httpOnly` flag is specified in the cookie definition.
218158
* The default is `false`.
219159
*/
220-
override predicate isHttpOnly() { allHaveCookieAttribute(httpOnlyFlag()) }
160+
override predicate isHttpOnly() { hasCookieAttribute(httpOnlyFlag()) }
221161

222162
/**
223-
* The predicate holds only if all elements have the specified attribute.
163+
* The predicate holds only if any element have the specified attribute.
224164
*/
225165
bindingset[attribute]
226-
private predicate allHaveCookieAttribute(string attribute) {
227-
forall(DataFlow::Node n | n = getCookieOptionsArgument() |
228-
exists(string s |
229-
n.mayHaveStringValue(s) and
230-
hasCookieAttribute(s, attribute)
231-
)
232-
or
233-
exists(AttributeToSetCookieHeaderTrackingConfig cfg, DataFlow::Node source |
234-
cfg.hasFlow(source, n) and
235-
exists(string attr |
236-
source.mayHaveStringValue(attr) and
237-
attr.regexpMatch("(?i).*\\b" + attribute + "\\b.*")
238-
)
239-
)
166+
private predicate hasCookieAttribute(string attribute) {
167+
exists(string s |
168+
this.mayHaveStringValue(s) and
169+
hasCookieAttribute(s, attribute)
240170
)
241171
}
242172

243173
/**
244-
* The predicate holds only if any element has a sensitive name and
245-
* doesn't have the `httpOnly` flag.
174+
* The predicate holds only if any element has a sensitive name.
246175
*/
247-
override predicate isAuthNotHttpOnly() {
248-
exists(DataFlow::Node n | n = getCookieOptionsArgument() |
249-
exists(string s |
250-
n.mayHaveStringValue(s) and
251-
(
252-
not hasCookieAttribute(s, httpOnlyFlag()) and
253-
regexpMatchAuth(getCookieName(s))
254-
)
255-
)
256-
or
257-
not exists(AttributeToSetCookieHeaderTrackingConfig cfg, DataFlow::Node source |
258-
cfg.hasFlow(source, n) and
259-
exists(string attr |
260-
source.mayHaveStringValue(attr) and
261-
attr.regexpMatch("(?i).*\\b" + httpOnlyFlag() + "\\b.*")
262-
)
263-
) and
264-
exists(SensitiveNameToSetCookieHeaderTrackingConfig cfg | cfg.hasFlow(_, n))
265-
)
176+
override predicate isSensitive() {
177+
HeuristicNames::nameIndicatesSensitiveData(getCookieName([
178+
any(string s | this.mayHaveStringValue(s)),
179+
this.(StringOps::ConcatenationRoot).getConstantStringParts()
180+
]), _)
266181
}
267182

268183
/**
@@ -271,7 +186,9 @@ module Cookie {
271186
* `<cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly`
272187
*/
273188
bindingset[s]
274-
private string getCookieName(string s) { result = s.regexpCapture("\\s*([^=\\s]*)\\s*=.*", 1) }
189+
private string getCookieName(string s) {
190+
result = s.regexpCapture("\\s*\\b([^=\\s]*)\\b\\s*=.*", 1)
191+
}
275192

276193
/**
277194
* Holds if the `Set-Cookie` header value contains the specified attribute
@@ -284,14 +201,14 @@ module Cookie {
284201
*/
285202
bindingset[s, attribute]
286203
private predicate hasCookieAttribute(string s, string attribute) {
287-
s.regexpMatch("(?i).*;\\s*" + attribute + "\\s*;?.*$")
204+
s.regexpMatch("(?i).*;\\s*" + attribute + "\\b\\s*;?.*$")
288205
}
289206
}
290207

291208
/**
292209
* A cookie set using `js-cookie` library (https://github.com/js-cookie/js-cookie).
293210
*/
294-
class InsecureJsCookie extends Cookie {
211+
class InsecureJsCookie extends CookieWrite {
295212
InsecureJsCookie() {
296213
this =
297214
[
@@ -301,9 +218,7 @@ module Cookie {
301218
].getAMemberCall("set")
302219
}
303220

304-
override string getKind() { result = "js-cookie" }
305-
306-
override DataFlow::SourceNode getCookieOptionsArgument() {
221+
DataFlow::SourceNode getCookieOptionsArgument() {
307222
result = this.(DataFlow::CallNode).getAnArgument().getALocalSource()
308223
}
309224

@@ -316,7 +231,7 @@ module Cookie {
316231
getCookieFlagValue(secureFlag()).mayHaveBooleanValue(true)
317232
}
318233

319-
override predicate isAuthNotHttpOnly() { none() }
234+
override predicate isSensitive() { none() }
320235

321236
override predicate isHttpOnly() { none() } // js-cookie is browser side library and doesn't support HttpOnly
322237
}

javascript/ql/test/query-tests/Security/CWE-1004/CookieWithoutHttpOnly.expected

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
| test_cookie-session.js:52:9:56:2 | session ... BAD\\n}) | Cookie attribute 'HttpOnly' is not set to true for this sensitive cookie. |
66
| test_express-session.js:11:9:15:2 | session ... BAD\\n}) | Cookie attribute 'HttpOnly' is not set to true for this sensitive cookie. |
77
| test_express-session.js:28:9:32:2 | session ... tter\\n}) | Cookie attribute 'HttpOnly' is not set to true for this sensitive cookie. |
8-
| test_httpserver.js:7:37:7:48 | "auth=ninja" | Cookie attribute 'HttpOnly' is not set to true for this sensitive cookie. |
9-
| test_httpserver.js:27:37:27:70 | ["auth= ... cript"] | Cookie attribute 'HttpOnly' is not set to true for this sensitive cookie. |
10-
| test_httpserver.js:57:37:57:80 | ["auth= ... cript"] | Cookie attribute 'HttpOnly' is not set to true for this sensitive cookie. |
11-
| test_httpserver.js:87:37:87:59 | `sessio ... {attr}` | Cookie attribute 'HttpOnly' is not set to true for this sensitive cookie. |
8+
| test_httpserver.js:7:37:7:51 | "authKey=ninja" | Cookie attribute 'HttpOnly' is not set to true for this sensitive cookie. |
9+
| test_httpserver.js:27:38:27:52 | "authKey=ninja" | Cookie attribute 'HttpOnly' is not set to true for this sensitive cookie. |
10+
| test_httpserver.js:87:37:87:59 | `authKe ... {attr}` | Cookie attribute 'HttpOnly' is not set to true for this sensitive cookie. |
1211
| test_responseCookie.js:15:5:20:10 | res.coo ... }) | Cookie attribute 'HttpOnly' is not set to true for this sensitive cookie. |
1312
| test_responseCookie.js:25:5:28:10 | res.coo ... }) | Cookie attribute 'HttpOnly' is not set to true for this sensitive cookie. |
1413
| test_responseCookie.js:48:5:48:43 | res.coo ... ptions) | Cookie attribute 'HttpOnly' is not set to true for this sensitive cookie. |

0 commit comments

Comments
 (0)