Skip to content

Commit cc527bd

Browse files
authored
Merge pull request #7721 from erik-krogh/CWE-1275
JS: add a js/samesite-none-cookie cookie
2 parents 26d9848 + caaee5e commit cc527bd

File tree

10 files changed

+293
-3
lines changed

10 files changed

+293
-3
lines changed

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

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ module CookieWrites {
2727
*/
2828
abstract predicate isSensitive();
2929

30+
/**
31+
* Gets the SameSite attribute of the cookie if present.
32+
* Either "Strict", "Lax" or "None".
33+
*/
34+
abstract string getSameSite();
35+
3036
/**
3137
* Holds if the cookie write happens on a server, i.e. the `httpOnly` flag is relevant.
3238
*/
@@ -95,6 +101,31 @@ private predicate hasCookieAttribute(string s, string attribute) {
95101
s.regexpMatch("(?i).*;\\s*" + attribute + "\\b\\s*;?.*$")
96102
}
97103

104+
/**
105+
* Gets the value for a `Set-Cookie` header attribute.
106+
*/
107+
bindingset[s, attribute]
108+
private string getCookieValue(string s, string attribute) {
109+
result = s.regexpCapture("(?i).*;\\s*" + attribute + "=(\\w+)\\b\\s*;?.*$", 1)
110+
}
111+
112+
/**
113+
* Gets the "SameSite" value for a given `node`.
114+
* Converts boolean values to the corresponding string value.
115+
*
116+
* Not all libraries support boolean values for the `SameSite` attribute,
117+
* but here we assume that they do.
118+
*/
119+
private string getSameSiteValue(DataFlow::Node node) {
120+
node.mayHaveStringValue(result)
121+
or
122+
node.mayHaveBooleanValue(true) and
123+
result = "Strict"
124+
or
125+
node.mayHaveBooleanValue(false) and
126+
result = "Lax"
127+
}
128+
98129
/**
99130
* A model of the `js-cookie` library (https://github.com/js-cookie/js-cookie).
100131
*/
@@ -105,7 +136,9 @@ private module JsCookie {
105136
DataFlow::CallNode libMemberCall(string name) {
106137
result = DataFlow::globalVarRef("Cookie").getAMemberCall(name) or
107138
result = DataFlow::globalVarRef("Cookie").getAMemberCall("noConflict").getAMemberCall(name) or
108-
result = DataFlow::moduleMember("js-cookie", name).getACall()
139+
result = DataFlow::moduleMember("js-cookie", name).getACall() or
140+
// es-cookie behaves basically the same as js-cookie
141+
result = DataFlow::moduleMember("es-cookie", name).getACall()
109142
}
110143

111144
class ReadAccess extends PersistentReadAccess, DataFlow::CallNode {
@@ -132,6 +165,10 @@ private module JsCookie {
132165
}
133166

134167
override predicate isSensitive() { canHaveSensitiveCookie(this.getArgument(0)) }
168+
169+
override string getSameSite() {
170+
result = getSameSiteValue(this.getOptionArgument(2, "sameSite"))
171+
}
135172
}
136173
}
137174

@@ -173,6 +210,15 @@ private module BrowserCookies {
173210
}
174211

175212
override predicate isSensitive() { canHaveSensitiveCookie(this.getArgument(0)) }
213+
214+
override string getSameSite() {
215+
result = getSameSiteValue(this.getOptionArgument(2, "samesite"))
216+
or
217+
// or, an explicit default has been set
218+
DataFlow::moduleMember("browser-cookies", "defaults")
219+
.getAPropertyWrite("samesite")
220+
.mayHaveStringValue(result)
221+
}
176222
}
177223
}
178224

@@ -211,6 +257,10 @@ private module LibCookie {
211257
}
212258

213259
override predicate isSensitive() { canHaveSensitiveCookie(this.getArgument(0)) }
260+
261+
override string getSameSite() {
262+
result = getSameSiteValue(this.getOptionArgument(2, "sameSite"))
263+
}
214264
}
215265
}
216266

@@ -242,6 +292,10 @@ private module ExpressCookies {
242292
not value.mayHaveBooleanValue(false) // anything but `false` is accepted as being maybe true
243293
)
244294
}
295+
296+
override string getSameSite() {
297+
result = getSameSiteValue(this.getOptionArgument(2, "sameSite"))
298+
}
245299
}
246300

247301
/**
@@ -268,6 +322,8 @@ private module ExpressCookies {
268322
// A cookie is httpOnly if the `httpOnly` flag is not explicitly set to `false`.
269323
not this.getCookieFlagValue(CookieWrites::httpOnly()).mayHaveBooleanValue(false)
270324
}
325+
326+
override string getSameSite() { result = getSameSiteValue(this.getCookieFlagValue("sameSite")) }
271327
}
272328

273329
/**
@@ -297,6 +353,8 @@ private module ExpressCookies {
297353
// A cookie is httpOnly if the `httpOnly` flag is not explicitly set to `false`.
298354
not this.getCookieFlagValue(CookieWrites::httpOnly()).mayHaveBooleanValue(false)
299355
}
356+
357+
override string getSameSite() { result = getSameSiteValue(this.getCookieFlagValue("sameSite")) }
300358
}
301359
}
302360

@@ -339,6 +397,8 @@ private class HTTPCookieWrite extends CookieWrites::CookieWrite {
339397
}
340398

341399
override predicate isSensitive() { canHaveSensitiveCookie(this) }
400+
401+
override string getSameSite() { result = getCookieValue(header, "SameSite") }
342402
}
343403

344404
/**
@@ -365,4 +425,6 @@ private class DocumentCookieWrite extends CookieWrites::ClientSideCookieWrite {
365425
}
366426

367427
override predicate isSensitive() { canHaveSensitiveCookie(write.getRhs()) }
428+
429+
override string getSameSite() { result = getCookieValue(cookie, "SameSite") }
368430
}

javascript/ql/src/Security/CWE-1004/ClientExposedCookie.qhelp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ Set the <code>httpOnly</code> flag on all cookies that are not needed by the cli
2323
The following example stores an authentication token in a cookie that can
2424
be viewed by the client.
2525
</p>
26-
<sample src="examples/ClientExposedCookieGood.js"/>
26+
<sample src="examples/ClientExposedCookieBad.js"/>
2727
<p>
2828
To force the cookie to be transmitted using SSL, set the <code>secure</code>
2929
attribute on the cookie.
3030
</p>
31-
<sample src="examples/ClientExposedCookieBad.js"/>
31+
<sample src="examples/ClientExposedCookieGood.js"/>
3232
</example>
3333

3434
<references>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Authentication cookies where the SameSite attribute is set to "None" can
9+
potentially be used to perform Cross-Site Request Forgery (CSRF) attacks
10+
if no other CSRF protections are in place.
11+
</p>
12+
<p>
13+
With SameSite set to "None", a third party website may create an authorized cross-site request
14+
that includes the cookie.
15+
Such a cross-site request can allow that website to perform actions on behalf of a user.
16+
</p>
17+
</overview>
18+
19+
<recommendation>
20+
<p>
21+
Set the <code>SameSite</code> attribute to <code>Strict</code> on all sensitive cookies.
22+
</p>
23+
</recommendation>
24+
25+
<example>
26+
<p>
27+
The following example stores an authentication token in a cookie where the <code>SameSite</code>
28+
attribute is set to <code>None</code>.
29+
</p>
30+
<sample src="examples/SameSiteCookieBad.js"/>
31+
<p>
32+
To prevent the cookie from being included in cross-site requests, set the <code>SameSite</code>
33+
attribute to <code>Strict</code>.
34+
</p>
35+
<sample src="examples/SameSiteCookieGood.js"/>
36+
</example>
37+
38+
<references>
39+
<li>MDN Web Docs: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite">SameSite cookies</a>.</li>
40+
<li>OWASP: <a href="https://owasp.org/www-community/SameSite">SameSite</a>.</li>
41+
</references>
42+
43+
</qhelp>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @name Sensitive cookie without SameSite restrictions
3+
* @description Sensitive cookies where the SameSite attribute is set to "None" can
4+
* in some cases allow for Cross-Site Request Forgery (CSRF) attacks.
5+
* @kind problem
6+
* @problem.severity warning
7+
* @security-severity 5.0
8+
* @precision medium
9+
* @id js/samesite-none-cookie
10+
* @tags security
11+
* external/cwe/cwe-1275
12+
*/
13+
14+
import javascript
15+
16+
from CookieWrites::CookieWrite cookie
17+
where
18+
cookie.isSensitive() and
19+
cookie.isSecure() and // `js/clear-text-cookie` will report it if the cookie is not secure.
20+
cookie.getSameSite().toLowerCase() = "none"
21+
select cookie, "Sensitive cookie with SameSite set to 'None'"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const http = require('http');
2+
3+
const server = http.createServer((req, res) => {
4+
res.setHeader("Set-Cookie", `authKey=${makeAuthkey()}; secure; httpOnly; SameSite=None`);
5+
res.writeHead(200, { 'Content-Type': 'text/html' });
6+
res.end('<h2>Hello world</h2>');
7+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const http = require('http');
2+
3+
const server = http.createServer((req, res) => {
4+
res.setHeader("Set-Cookie", `authKey=${makeAuthkey()}; secure; httpOnly; SameSite=Strict`);
5+
res.writeHead(200, { 'Content-Type': 'text/html' });
6+
res.end('<h2>Hello world</h2>');
7+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: newQuery
3+
---
4+
* A new query `js/samesite-none-cookie` has been added. The query detects when the SameSite attribute is set to None on a sensitive cookie.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
| tst-sameSite.js:4:3:8:4 | Cookies ... OK\\n }) | Sensitive cookie with SameSite set to 'None' |
2+
| tst-sameSite.js:20:3:25:4 | cookies ... OK\\n }) | Sensitive cookie with SameSite set to 'None' |
3+
| tst-sameSite.js:38:19:43:4 | cookie. ... ",\\n }) | Sensitive cookie with SameSite set to 'None' |
4+
| tst-sameSite.js:58:3:63:4 | res.coo ... OK\\n }) | Sensitive cookie with SameSite set to 'None' |
5+
| tst-sameSite.js:76:3:82:4 | session ... OK\\n }) | Sensitive cookie with SameSite set to 'None' |
6+
| tst-sameSite.js:98:3:106:4 | express ... },\\n }) | Sensitive cookie with SameSite set to 'None' |
7+
| tst-sameSite.js:126:33:126:70 | "authKe ... Secure" | Sensitive cookie with SameSite set to 'None' |
8+
| tst-sameSite.js:134:3:134:17 | document.cookie | Sensitive cookie with SameSite set to 'None' |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security/CWE-1275/SameSiteNoneCookie.ql
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import * as Cookies from "es-cookie";
2+
3+
function esCookies() {
4+
Cookies.set("authkey", "value", {
5+
secure: true,
6+
httpOnly: true,
7+
sameSite: "None", // NOT OK
8+
});
9+
10+
Cookies.set("authkey", "value", {
11+
secure: true,
12+
httpOnly: true,
13+
sameSite: "Strict", // OK
14+
});
15+
}
16+
17+
function browserCookies() {
18+
var cookies = require("browser-cookies");
19+
20+
cookies.set("authkey", "value", {
21+
expires: 365,
22+
secure: true,
23+
httponly: true,
24+
samesite: "None", // NOT OK
25+
});
26+
27+
cookies.set("authkey", "value", {
28+
expires: 365,
29+
secure: true,
30+
httponly: true,
31+
samesite: "Strict", // OK
32+
});
33+
}
34+
35+
function cookie() {
36+
var cookie = require("cookie");
37+
38+
var setCookie = cookie.serialize("authkey", "value", {
39+
maxAge: 9000000000,
40+
httpOnly: true,
41+
secure: true,
42+
sameSite: "None",
43+
});
44+
45+
var setCookie = cookie.serialize("authkey", "value", {
46+
maxAge: 9000000000,
47+
httpOnly: true,
48+
secure: true,
49+
sameSite: true, // OK
50+
});
51+
}
52+
53+
const express = require("express");
54+
const app = express();
55+
const session = require("cookie-session");
56+
57+
app.get("/a", function (req, res, next) {
58+
res.cookie("authkey", "value", {
59+
maxAge: 9000000000,
60+
httpOnly: true,
61+
secure: true,
62+
sameSite: "None", // NOT OK
63+
});
64+
65+
res.cookie("session", "value", {
66+
maxAge: 9000000000,
67+
httpOnly: true,
68+
secure: true,
69+
sameSite: "Strict", // OK
70+
});
71+
72+
res.end("ok");
73+
});
74+
75+
app.use(
76+
session({
77+
name: "session",
78+
keys: ["key1", "key2"],
79+
httpOnly: true,
80+
secure: true,
81+
sameSite: "None", // NOT OK
82+
})
83+
);
84+
85+
app.use(
86+
session({
87+
name: "session",
88+
keys: ["key1", "key2"],
89+
httpOnly: true,
90+
secure: true,
91+
sameSite: "Strict", // OK
92+
})
93+
);
94+
95+
const expressSession = require("express-session");
96+
97+
app.use(
98+
expressSession({
99+
name: "session",
100+
keys: ["key1", "key2"],
101+
cookie: {
102+
httpOnly: true,
103+
secure: true,
104+
sameSite: "None", // NOT OK
105+
},
106+
})
107+
);
108+
109+
app.use(
110+
expressSession({
111+
name: "session",
112+
keys: ["key1", "key2"],
113+
cookie: {
114+
httpOnly: true,
115+
secure: true,
116+
sameSite: "Strict", // OK
117+
},
118+
})
119+
);
120+
121+
const http = require("http");
122+
123+
function test1() {
124+
const server = http.createServer((req, res) => {
125+
res.setHeader("Content-Type", "text/html");
126+
res.setHeader("Set-Cookie", "authKey=ninja; SameSite=None; Secure"); // NOT OK
127+
res.setHeader("Set-Cookie", "authKey=ninja; SameSite=Strict; Secure"); // OK
128+
res.writeHead(200, { "Content-Type": "text/plain" });
129+
res.end("ok");
130+
});
131+
}
132+
133+
function documentCookie() {
134+
document.cookie = "authKey=ninja; SameSite=None; Secure"; // NOT OK
135+
document.cookie = "authKey=ninja; SameSite=Strict; Secure"; // OK
136+
}
137+

0 commit comments

Comments
 (0)