Skip to content

Commit 283b823

Browse files
committed
add more cookie models
1 parent 2cb3d2c commit 283b823

File tree

3 files changed

+96
-26
lines changed

3 files changed

+96
-26
lines changed

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

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ module CookieWrites {
5959
/**
6060
* A model of the `js-cookie` library (https://github.com/js-cookie/js-cookie).
6161
*/
62-
// TODO: Writes to document.cookie.
6362
private module JsCookie {
6463
/**
6564
* Gets a function call that invokes method `name` of the `js-cookie` library.
@@ -118,13 +117,27 @@ private module BrowserCookies {
118117
}
119118
}
120119

121-
class WriteAccess extends PersistentWriteAccess, DataFlow::CallNode {
122-
// TODO: CookieWrite
120+
class WriteAccess extends PersistentWriteAccess, DataFlow::CallNode,
121+
CookieWrites::ClientSideCookieWrite {
123122
WriteAccess() { this = libMemberCall("set") }
124123

125124
string getKey() { getArgument(0).mayHaveStringValue(result) }
126125

127126
override DataFlow::Node getValue() { result = getArgument(1) }
127+
128+
override predicate isSecure() {
129+
// A cookie is secure if there are cookie options with the `secure` flag set to `true`.
130+
this.getOptionArgument(2, CookieWrites::secure()).mayHaveBooleanValue(true)
131+
or
132+
// or, an explicit default has been set
133+
exists(DataFlow::moduleMember("browser-cookies", "defaults").getAPropertyWrite("secure"))
134+
}
135+
136+
override predicate isSensitive() {
137+
HeuristicNames::nameIndicatesSensitiveData(any(string s |
138+
this.getArgument(0).mayHaveStringValue(s)
139+
), _)
140+
}
128141
}
129142
}
130143

@@ -147,13 +160,24 @@ private module LibCookie {
147160
override PersistentWriteAccess getAWrite() { key = result.(WriteAccess).getKey() }
148161
}
149162

150-
class WriteAccess extends PersistentWriteAccess, DataFlow::CallNode {
151-
// TODO: CookieWrite
163+
class WriteAccess extends PersistentWriteAccess, DataFlow::CallNode,
164+
CookieWrites::ClientSideCookieWrite {
152165
WriteAccess() { this = libMemberCall("serialize") }
153166

154167
string getKey() { getArgument(0).mayHaveStringValue(result) }
155168

156169
override DataFlow::Node getValue() { result = getArgument(1) }
170+
171+
override predicate isSecure() {
172+
// A cookie is secure if there are cookie options with the `secure` flag set to `true`.
173+
this.getOptionArgument(2, CookieWrites::secure()).mayHaveBooleanValue(true)
174+
}
175+
176+
override predicate isSensitive() {
177+
HeuristicNames::nameIndicatesSensitiveData(any(string s |
178+
this.getArgument(0).mayHaveStringValue(s)
179+
), _)
180+
}
157181
}
158182
}
159183

@@ -286,28 +310,56 @@ private class HTTPCookieWrite extends CookieWrites::CookieWrite {
286310
override predicate isSensitive() {
287311
HeuristicNames::nameIndicatesSensitiveData(getCookieName(header), _)
288312
}
313+
}
289314

290-
/**
291-
* Gets cookie name from a `Set-Cookie` header value.
292-
* The header value always starts with `<cookie-name>=<cookie-value>` optionally followed by attributes:
293-
* `<cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly`
294-
*/
295-
bindingset[s]
296-
private string getCookieName(string s) {
297-
result = s.regexpCapture("\\s*\\b([^=\\s]*)\\b\\s*=.*", 1)
315+
/**
316+
* Gets cookie name from a `Set-Cookie` header value.
317+
* The header value always starts with `<cookie-name>=<cookie-value>` optionally followed by attributes:
318+
* `<cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly`
319+
*/
320+
bindingset[s]
321+
private string getCookieName(string s) {
322+
result = s.regexpCapture("\\s*\\b([^=\\s]*)\\b\\s*=.*", 1)
323+
}
324+
325+
/**
326+
* Holds if the `Set-Cookie` header value contains the specified attribute
327+
* 1. The attribute is case insensitive
328+
* 2. It always starts with a pair `<cookie-name>=<cookie-value>`.
329+
* If the attribute is present there must be `;` after the pair.
330+
* Other attributes like `Domain=`, `Path=`, etc. may come after the pair:
331+
* `<cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly`
332+
* See `https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie`
333+
*/
334+
bindingset[s, attribute]
335+
private predicate hasCookieAttribute(string s, string attribute) {
336+
s.regexpMatch("(?i).*;\\s*" + attribute + "\\b\\s*;?.*$")
337+
}
338+
339+
/**
340+
* A write to `document.cookie`.
341+
*/
342+
private class DocumentCookieWrite extends CookieWrites::ClientSideCookieWrite {
343+
string cookie;
344+
345+
DocumentCookieWrite() {
346+
exists(DataFlow::PropWrite write | this = write |
347+
write = DOM::documentRef().getAPropertyWrite("cookie") and
348+
cookie =
349+
[
350+
any(string s | write.getRhs().mayHaveStringValue(s)),
351+
write.getRhs().(StringOps::ConcatenationRoot).getConstantStringParts()
352+
]
353+
)
298354
}
299355

300-
/**
301-
* Holds if the `Set-Cookie` header value contains the specified attribute
302-
* 1. The attribute is case insensitive
303-
* 2. It always starts with a pair `<cookie-name>=<cookie-value>`.
304-
* If the attribute is present there must be `;` after the pair.
305-
* Other attributes like `Domain=`, `Path=`, etc. may come after the pair:
306-
* `<cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly`
307-
* See `https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie`
308-
*/
309-
bindingset[s, attribute]
310-
private predicate hasCookieAttribute(string s, string attribute) {
311-
s.regexpMatch("(?i).*;\\s*" + attribute + "\\b\\s*;?.*$")
356+
override predicate isSecure() {
357+
// A cookie is secure if the `secure` flag is specified in the cookie definition.
358+
// The default is `false`.
359+
hasCookieAttribute(cookie, CookieWrites::secure())
360+
}
361+
362+
override predicate isSensitive() {
363+
HeuristicNames::nameIndicatesSensitiveData(getCookieName(cookie), _)
312364
}
313365
}

javascript/ql/test/query-tests/Security/CWE-614/ClearTextCookie.expected

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@
1010
| tst-cleartextCookie.js:124:9:124:21 | session(sess) | Sensitive cookie sent without enforcing SSL encryption |
1111
| tst-cleartextCookie.js:148:9:156:2 | session ... Date\\n}) | Sensitive cookie sent without enforcing SSL encryption |
1212
| tst-cleartextCookie.js:160:33:160:58 | `authKe ... key()}` | Sensitive cookie sent without enforcing SSL encryption |
13+
| tst-cleartextCookie.js:173:5:173:19 | document.cookie | Sensitive cookie sent without enforcing SSL encryption |
14+
| tst-cleartextCookie.js:177:5:177:41 | cookies ... hkey()) | Sensitive cookie sent without enforcing SSL encryption |
15+
| tst-cleartextCookie.js:182:5:182:46 | cookie. ... hkey()) | Sensitive cookie sent without enforcing SSL encryption |

javascript/ql/test/query-tests/Security/CWE-614/tst-cleartextCookie.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,19 @@ http.createServer((req, res) => {
166166
res.setHeader("Set-Cookie", `authKey=${makeAuthkey()}; secure; httpOnly`); // OK
167167
res.writeHead(200, { 'Content-Type': 'text/html' });
168168
res.end('<h2>Hello world</h2>');
169-
});
169+
});
170+
171+
function clientCookies() {
172+
document.cookie = `authKey=${makeAuthkey()}; secure`; // OK
173+
document.cookie = `authKey=${makeAuthkey()}`; // NOT OK
174+
175+
var cookies = require('browser-cookies');
176+
177+
cookies.set('authKey', makeAuthkey()); // NOT OK
178+
cookies.set('authKey', makeAuthkey(), { secure: true, expires: 7 }); // OK
179+
180+
const cookie = require('cookie');
181+
182+
cookie.serialize('authKey', makeAuthkey()); // NOT OK
183+
cookie.serialize('authKey', makeAuthkey(), { secure: true, expires: 7 }); // OK
184+
}

0 commit comments

Comments
 (0)