Skip to content

Commit ac94869

Browse files
authored
Merge pull request #3978 from dellalibera/js/insecure-cookies
Approved by esbena
2 parents ceddc24 + 9aa1404 commit ac94869

File tree

10 files changed

+319
-0
lines changed

10 files changed

+319
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>Failing to set the 'secure' flag on a cookie can cause it to be sent in cleartext.
7+
This makes it easier for an attacker to intercept.</p>
8+
</overview>
9+
<recommendation>
10+
11+
<p>Always set the <code>secure</code> flag to `true` on a cookie before adding it
12+
to an HTTP response (if the default value is `false`).</p>
13+
14+
</recommendation>
15+
16+
<references>
17+
18+
<li>Production Best Practices: Security:<a href="https://expressjs.com/en/advanced/best-practice-security.html#use-cookies-securely">Use cookies securely</a>.</li>
19+
<li>NodeJS security cheat sheet:<a href="https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html#set-cookie-flags-appropriately">Set cookie flags appropriately</a>.</li>
20+
<li>express-session:<a href="https://github.com/expressjs/session#cookiesecure">cookie.secure</a>.</li>
21+
<li>cookie-session:<a href="https://github.com/expressjs/cookie-session#cookie-options">Cookie Options</a>.</li>
22+
<li><a href="https://expressjs.com/en/api.html#res.cookie">express response.cookie</a>.</li>
23+
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie">Set-Cookie</a>.</li>
24+
<li><a href="https://github.com/js-cookie/js-cookie">js-cookie</a>.</li>
25+
</references>
26+
</qhelp>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @name Failure to set secure cookies
3+
* @description Insecure cookies may be sent in cleartext, which makes them vulnerable to
4+
* interception.
5+
* @kind problem
6+
* @problem.severity error
7+
* @precision high
8+
* @id js/insecure-cookie
9+
* @tags security
10+
* external/cwe/cwe-614
11+
*/
12+
13+
import javascript
14+
import InsecureCookie::Cookie
15+
16+
from Cookie cookie
17+
where not cookie.isSecure()
18+
select cookie, "Cookie is added to response without the 'secure' flag being set to true"
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* Provides classes for reasoning about cookies added to response without the 'secure' flag being set.
3+
* A cookie without the 'secure' flag being set can be intercepted and read by a malicious user.
4+
*/
5+
6+
import javascript
7+
8+
module Cookie {
9+
/**
10+
* `secure` property of the cookie options.
11+
*/
12+
string flag() { result = "secure" }
13+
14+
/**
15+
* Abstract class to represent different cases of insecure cookie settings.
16+
*/
17+
abstract class Cookie extends DataFlow::Node {
18+
/**
19+
* Gets the name of the middleware/library used to set the cookie.
20+
*/
21+
abstract string getKind();
22+
23+
/**
24+
* Gets the options used to set this cookie, if any.
25+
*/
26+
abstract DataFlow::Node getCookieOptionsArgument();
27+
28+
/**
29+
* Holds if this cookie is secure.
30+
*/
31+
abstract predicate isSecure();
32+
}
33+
34+
/**
35+
* A cookie set using the `express` module `cookie-session` (https://github.com/expressjs/cookie-session).
36+
*/
37+
class InsecureCookieSession extends ExpressLibraries::CookieSession::MiddlewareInstance, Cookie {
38+
override string getKind() { result = "cookie-session" }
39+
40+
override DataFlow::SourceNode getCookieOptionsArgument() { result = this.getOption("cookie") }
41+
42+
private DataFlow::Node getCookieFlagValue(string flag) {
43+
result = this.getCookieOptionsArgument().getAPropertyWrite(flag).getRhs()
44+
}
45+
46+
override predicate isSecure() {
47+
// The flag `secure` is set to `false` by default for HTTP, `true` by default for HTTPS (https://github.com/expressjs/cookie-session#cookie-options).
48+
// A cookie is secure if the `secure` flag is not explicitly set to `false`.
49+
not getCookieFlagValue(flag()).mayHaveBooleanValue(false)
50+
}
51+
}
52+
53+
/**
54+
* A cookie set using the `express` module `express-session` (https://github.com/expressjs/session).
55+
*/
56+
class InsecureExpressSessionCookie extends ExpressLibraries::ExpressSession::MiddlewareInstance,
57+
Cookie {
58+
override string getKind() { result = "express-session" }
59+
60+
override DataFlow::SourceNode getCookieOptionsArgument() { result = this.getOption("cookie") }
61+
62+
private DataFlow::Node getCookieFlagValue(string flag) {
63+
result = this.getCookieOptionsArgument().getAPropertyWrite(flag).getRhs()
64+
}
65+
66+
override predicate isSecure() {
67+
// The flag `secure` is not set by default (https://github.com/expressjs/session#Cookieecure).
68+
// The default value for cookie options is { path: '/', httpOnly: true, secure: false, maxAge: null }.
69+
// A cookie is secure if there are the cookie options with the `secure` flag set to `true` or to `auto`.
70+
getCookieFlagValue(flag()).mayHaveBooleanValue(true) or
71+
getCookieFlagValue(flag()).mayHaveStringValue("auto")
72+
}
73+
}
74+
75+
/**
76+
* A cookie set using `response.cookie` from `express` module (https://expressjs.com/en/api.html#res.cookie).
77+
*/
78+
class InsecureExpressCookieResponse extends Cookie, DataFlow::MethodCallNode {
79+
InsecureExpressCookieResponse() { this.calls(any(Express::ResponseExpr r).flow(), "cookie") }
80+
81+
override string getKind() { result = "response.cookie" }
82+
83+
override DataFlow::SourceNode getCookieOptionsArgument() {
84+
result = this.getLastArgument().getALocalSource()
85+
}
86+
87+
private DataFlow::Node getCookieFlagValue(string flag) {
88+
result = this.getCookieOptionsArgument().getAPropertyWrite(flag).getRhs()
89+
}
90+
91+
override predicate isSecure() {
92+
// A cookie is secure if there are cookie options with the `secure` flag set to `true`.
93+
getCookieFlagValue(flag()).mayHaveBooleanValue(true)
94+
}
95+
}
96+
97+
/**
98+
* A cookie set using `Set-Cookie` header of an `HTTP` response (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).
99+
*/
100+
class InsecureSetCookieHeader extends Cookie {
101+
InsecureSetCookieHeader() {
102+
this.asExpr() = any(HTTP::SetCookieHeader setCookie).getHeaderArgument()
103+
}
104+
105+
override string getKind() { result = "set-cookie header" }
106+
107+
override DataFlow::Node getCookieOptionsArgument() {
108+
result.asExpr() = this.asExpr().(ArrayExpr).getAnElement()
109+
}
110+
111+
override predicate isSecure() {
112+
// A cookie is secure if the 'secure' flag is specified in the cookie definition.
113+
exists(string s |
114+
getCookieOptionsArgument().mayHaveStringValue(s) and
115+
s.regexpMatch("(.*;)?\\s*secure.*")
116+
)
117+
}
118+
}
119+
120+
/**
121+
* A cookie set using `js-cookie` library (https://github.com/js-cookie/js-cookie).
122+
*/
123+
class InsecureJsCookie extends Cookie {
124+
InsecureJsCookie() {
125+
this =
126+
[DataFlow::globalVarRef("Cookie"),
127+
DataFlow::globalVarRef("Cookie").getAMemberCall("noConflict"),
128+
DataFlow::moduleImport("js-cookie")].getAMemberCall("set")
129+
}
130+
131+
override string getKind() { result = "js-cookie" }
132+
133+
override DataFlow::SourceNode getCookieOptionsArgument() {
134+
result = this.(DataFlow::CallNode).getAnArgument().getALocalSource()
135+
}
136+
137+
DataFlow::Node getCookieFlagValue(string flag) {
138+
result = this.getCookieOptionsArgument().getAPropertyWrite(flag).getRhs()
139+
}
140+
141+
override predicate isSecure() {
142+
// A cookie is secure if there are cookie options with the `secure` flag set to `true`.
143+
getCookieFlagValue(flag()).mayHaveBooleanValue(true)
144+
}
145+
}
146+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
| test_cookie-session.js:18:9:28:2 | session ... }\\n}) | Cookie is added to response without the 'secure' flag being set to true |
2+
| test_express-session.js:5:9:8:2 | session ... T OK\\n}) | Cookie is added to response without the 'secure' flag being set to true |
3+
| test_express-session.js:10:9:13:2 | session ... T OK\\n}) | Cookie is added to response without the 'secure' flag being set to true |
4+
| test_express-session.js:15:9:18:2 | session ... T OK\\n}) | Cookie is added to response without the 'secure' flag being set to true |
5+
| test_express-session.js:25:9:25:21 | session(sess) | Cookie is added to response without the 'secure' flag being set to true |
6+
| test_httpserver.js:7:37:7:73 | ["type= ... cript"] | Cookie is added to response without the 'secure' flag being set to true |
7+
| test_jscookie.js:2:1:2:48 | js_cook ... alse }) | Cookie is added to response without the 'secure' flag being set to true |
8+
| test_responseCookie.js:5:5:10:10 | res.coo ... }) | Cookie is added to response without the 'secure' flag being set to true |
9+
| test_responseCookie.js:20:5:20:40 | res.coo ... ptions) | Cookie is added to response without the 'secure' flag being set to true |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/Security/CWE-614/InsecureCookie.ql
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const express = require('express')
2+
const app = express()
3+
const session = require('cookie-session')
4+
const expiryDate = new Date(Date.now() + 60 * 60 * 1000)
5+
6+
app.use(session({
7+
name: 'session',
8+
keys: ['key1', 'key2'],
9+
cookie: {
10+
secure: true, // OK
11+
httpOnly: true,
12+
domain: 'example.com',
13+
path: 'foo/bar',
14+
expires: expiryDate
15+
}
16+
}))
17+
18+
app.use(session({
19+
name: 'session',
20+
keys: ['key1', 'key2'],
21+
cookie: {
22+
secure: false, // NOT OK
23+
httpOnly: true,
24+
domain: 'example.com',
25+
path: 'foo/bar',
26+
expires: expiryDate
27+
}
28+
}))
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const express = require('express')
2+
const app = express()
3+
const session = require('express-session')
4+
5+
app.use(session({
6+
secret: 'secret',
7+
cookie: { secure: false } // NOT OK
8+
}))
9+
10+
app.use(session({
11+
secret: 'secret'
12+
// NOT OK
13+
}))
14+
15+
app.use(session({
16+
secret: 'secret',
17+
cookie: {} // NOT OK
18+
}))
19+
20+
const sess = {
21+
secret: 'secret',
22+
cookie: { secure: false } // NOT OK
23+
}
24+
25+
app.use(session(sess))
26+
27+
28+
app.set('trust proxy', 1)
29+
app.use(session({
30+
secret: 'secret',
31+
cookie: { secure: true } // OK
32+
}))
33+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const http = require('http');
2+
3+
function test1() {
4+
const server = http.createServer((req, res) => {
5+
res.setHeader('Content-Type', 'text/html');
6+
// NOT OK
7+
res.setHeader("Set-Cookie", ["type=ninja", "language=javascript"]);
8+
res.writeHead(200, { 'Content-Type': 'text/plain' });
9+
res.end('ok');
10+
});
11+
}
12+
13+
14+
function test2() {
15+
const server = http.createServer((req, res) => {
16+
res.setHeader('Content-Type', 'text/html');
17+
// OK
18+
res.setHeader("Set-Cookie", ["type=ninja; Secure", "language=javascript; secure"]);
19+
res.writeHead(200, { 'Content-Type': 'text/plain' });
20+
res.end('ok');
21+
});
22+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const js_cookie = require('js-cookie')
2+
js_cookie.set('key', 'value', { secure: false }); // NOT OK
3+
js_cookie.set('key', 'value', { secure: true }); // OK
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const express = require('express')
2+
const app = express()
3+
4+
app.get('/a', function (req, res, next) {
5+
res.cookie('name', 'value',
6+
{
7+
maxAge: 9000000000,
8+
httpOnly: true,
9+
secure: false // NOT OK
10+
});
11+
res.end('ok')
12+
})
13+
14+
app.get('/b', function (req, res, next) {
15+
let options = {
16+
maxAge: 9000000000,
17+
httpOnly: true,
18+
secure: false // NOT OK
19+
}
20+
res.cookie('name', 'value', options);
21+
res.end('ok')
22+
})
23+
24+
app.get('/c', function (req, res, next) {
25+
res.cookie('name', 'value',
26+
{
27+
maxAge: 9000000000,
28+
httpOnly: true,
29+
secure: true // OK
30+
});
31+
res.end('ok')
32+
})
33+

0 commit comments

Comments
 (0)