Skip to content

Commit fb90c2b

Browse files
authored
Merge pull request #2681 from asger-semmle/csrf-only-session-cookie-access
Approved by erik-krogh, max-schaefer
2 parents 27b5902 + 701d998 commit fb90c2b

File tree

9 files changed

+125
-6
lines changed

9 files changed

+125
-6
lines changed

change-notes/1.24/analysis-javascript.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
| Unbound event handler receiver (`js/unbound-event-handler-receiver`) | Fewer false positive results | This query now recognizes additional ways event handler receivers can be bound. |
3939
| Expression has no effect (`js/useless-expression`) | Fewer false positive results | The query now recognizes block-level flow type annotations and ignores the first statement of a try block. |
4040
| Use of call stack introspection in strict mode (`js/strict-mode-call-stack-introspection`) | Fewer false positive results | The query no longer flags expression statements. |
41+
| Missing CSRF middleware (`js/missing-token-validation`) | Fewer false positive results | The query reports fewer duplicates and only flags handlers that explicitly access cookie data. |
4142

4243
## Changes to libraries
4344

javascript/ql/src/Security/CWE-352/MissingCsrfMiddleware.ql

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,56 @@
1212

1313
import javascript
1414

15+
/** Gets a property name of `req` which refers to data usually derived from cookie data. */
16+
string cookieProperty() {
17+
result = "session" or result = "cookies" or result = "user"
18+
}
19+
20+
/** Gets a data flow node that flows to the base of an access to `cookies`, `session`, or `user`. */
21+
private DataFlow::SourceNode nodeLeadingToCookieAccess(DataFlow::TypeBackTracker t) {
22+
t.start() and
23+
exists(DataFlow::PropRead value |
24+
value = result.getAPropertyRead(cookieProperty()).getAPropertyRead() and
25+
26+
// Ignore accesses to values that are part of a CSRF or captcha check
27+
not value.getPropertyName().regexpMatch("(?i).*(csrf|xsrf|captcha).*") and
28+
29+
// Ignore calls like `req.session.save()`
30+
not value = any(DataFlow::InvokeNode call).getCalleeNode()
31+
)
32+
or
33+
exists(DataFlow::TypeBackTracker t2 |
34+
result = nodeLeadingToCookieAccess(t2).backtrack(t2, t)
35+
)
36+
}
37+
38+
/** Gets a data flow node that flows to the base of an access to `cookies`, `session`, or `user`. */
39+
DataFlow::SourceNode nodeLeadingToCookieAccess() {
40+
result = nodeLeadingToCookieAccess(DataFlow::TypeBackTracker::end())
41+
}
42+
43+
/**
44+
* Holds if `handler` uses cookies.
45+
*/
46+
predicate isRouteHandlerUsingCookies(Express::RouteHandler handler) {
47+
DataFlow::parameterNode(handler.getRequestParameter()) = nodeLeadingToCookieAccess()
48+
}
49+
50+
/** Gets a data flow node referring to a route handler that uses cookies. */
51+
private DataFlow::SourceNode getARouteUsingCookies(DataFlow::TypeTracker t) {
52+
t.start() and
53+
isRouteHandlerUsingCookies(result)
54+
or
55+
exists(DataFlow::TypeTracker t2 |
56+
result = getARouteUsingCookies(t2).track(t2, t)
57+
)
58+
}
59+
60+
/** Gets a data flow node referring to a route handler that uses cookies. */
61+
DataFlow::SourceNode getARouteUsingCookies() {
62+
result = getARouteUsingCookies(DataFlow::TypeTracker::end())
63+
}
64+
1565
/**
1666
* Checks if `expr` is preceded by the cookie middleware `cookie`.
1767
*
@@ -63,11 +113,23 @@ from
63113
where
64114
router = setup.getRouter() and
65115
handler = setup.getARouteHandlerExpr() and
116+
117+
// Require that the handler uses cookies and has cookie middleware.
118+
//
119+
// In practice, handlers that use cookies always have the cookie middleware or
120+
// the handler wouldn't work. However, if we can't find the cookie middleware, it
121+
// indicates that our middleware model is too incomplete, so in that case we
122+
// don't trust it to detect the presence of CSRF middleware either.
123+
getARouteUsingCookies().flowsToExpr(handler) and
66124
hasCookieMiddleware(handler, cookie) and
125+
126+
// Only flag the cookie parser registered first.
127+
not hasCookieMiddleware(cookie, _) and
128+
67129
not hasCsrfMiddleware(handler) and
68130
// Only warn for the last handler in a chain.
69131
handler.isLastHandler() and
70-
// Only warn for dangerous for handlers, such as for POST and PUT.
132+
// Only warn for dangerous handlers, such as for POST and PUT.
71133
not setup.getRequestMethod().isSafe()
72134
select cookie, "This cookie middleware is serving a request handler $@ without CSRF protection.",
73135
handler, "here"
Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
| MissingCsrfMiddlewareBad.js:7:9:7:22 | cookieParser() | This cookie middleware is serving a request handler $@ without CSRF protection. | MissingCsrfMiddlewareBad.js:10:26:11:1 | functio ... es) {\\n} | here |
2-
| csurf_api_example.js:39:37:39:50 | cookieParser() | This cookie middleware is serving a request handler $@ without CSRF protection. | csurf_api_example.js:39:53:41:3 | functio ... e')\\n } | here |
3-
| csurf_example.js:18:9:18:22 | cookieParser() | This cookie middleware is serving a request handler $@ without CSRF protection. | csurf_example.js:29:40:31:1 | functio ... sed')\\n} | here |
4-
| lusca_example.js:9:9:9:22 | cookieParser() | This cookie middleware is serving a request handler $@ without CSRF protection. | lusca_example.js:23:42:25:1 | functio ... sed')\\n} | here |
5-
| lusca_example.js:9:9:9:22 | cookieParser() | This cookie middleware is serving a request handler $@ without CSRF protection. | lusca_example.js:27:40:29:1 | functio ... sed')\\n} | here |
1+
| MissingCsrfMiddlewareBad.js:7:9:7:22 | cookieParser() | This cookie middleware is serving a request handler $@ without CSRF protection. | MissingCsrfMiddlewareBad.js:10:26:12:1 | functio ... il"];\\n} | here |
2+
| csurf_api_example.js:42:37:42:50 | cookieParser() | This cookie middleware is serving a request handler $@ without CSRF protection. | csurf_api_example.js:42:53:45:3 | functio ... e')\\n } | here |
3+
| csurf_example.js:18:9:18:22 | cookieParser() | This cookie middleware is serving a request handler $@ without CSRF protection. | csurf_example.js:31:40:34:1 | functio ... sed')\\n} | here |
4+
| lusca_example.js:9:9:9:22 | cookieParser() | This cookie middleware is serving a request handler $@ without CSRF protection. | lusca_example.js:26:42:29:1 | functio ... sed')\\n} | here |
5+
| lusca_example.js:9:9:9:22 | cookieParser() | This cookie middleware is serving a request handler $@ without CSRF protection. | lusca_example.js:31:40:34:1 | functio ... sed')\\n} | here |
6+
| unused_cookies.js:6:9:6:22 | cookieParser() | This cookie middleware is serving a request handler $@ without CSRF protection. | unused_cookies.js:8:34:13:1 | (req, r ... Ok');\\n} | here |
7+
| unused_cookies.js:6:9:6:22 | cookieParser() | This cookie middleware is serving a request handler $@ without CSRF protection. | unused_cookies.js:29:19:32:1 | (req, r ... Ok');\\n} | here |
8+
| unused_cookies.js:6:9:6:22 | cookieParser() | This cookie middleware is serving a request handler $@ without CSRF protection. | unused_cookies.js:34:22:37:1 | (req, r ... Ok');\\n} | here |

javascript/ql/test/query-tests/Security/CWE-352/MissingCsrfMiddlewareBad.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ app.use(cookieParser())
88
app.use(passport.authorize({ session: true }))
99

1010
app.post('/changeEmail', function (req, res) {
11+
let newEmail = req.cookies["newEmail"];
1112
})

javascript/ql/test/query-tests/Security/CWE-352/MissingCsrfMiddlewareGood.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ app.use(passport.authorize({ session: true }))
1010
app.use(csrf({ cookie:true }))
1111

1212
app.post('/changeEmail', function (req, res) {
13+
let newEmail = req.cookies["newEmail"];
1314
})

javascript/ql/test/query-tests/Security/CWE-352/csurf_api_example.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,26 @@ app.use(cookieParser())
2121
app.use(csrf({ cookie: true }))
2222

2323
app.get('/form', function (req, res) {
24+
let newEmail = req.cookies["newEmail"];
2425
// pass the csrfToken to the view
2526
res.render('send', { csrfToken: req.csrfToken() })
2627
})
2728

2829
app.post('/process', function (req, res) { // OK
30+
let newEmail = req.cookies["newEmail"];
2931
res.send('csrf was required to get here')
3032
})
3133

3234
function createApiRouter () {
3335
var router = new express.Router()
3436

3537
router.post('/getProfile', function (req, res) { // OK - cookies are not parsed
38+
let newEmail = req.cookies["newEmail"];
3639
res.send('no csrf to get here')
3740
})
3841

3942
router.post('/getProfile_unsafe', cookieParser(), function (req, res) { // NOT OK - may use cookies
43+
let newEmail = req.cookies["newEmail"];
4044
res.send('no csrf to get here')
4145
})
4246

javascript/ql/test/query-tests/Security/CWE-352/csurf_example.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@ var app = express()
1818
app.use(cookieParser())
1919

2020
app.get('/form', csrfProtection, function (req, res) { // OK
21+
let newEmail = req.cookies["newEmail"];
2122
// pass the csrfToken to the view
2223
res.render('send', { csrfToken: req.csrfToken() })
2324
})
2425

2526
app.post('/process', parseForm, csrfProtection, function (req, res) { // OK
27+
let newEmail = req.cookies["newEmail"];
2628
res.send('data is being processed')
2729
})
2830

2931
app.post('/process_unsafe', parseForm, function (req, res) { // NOT OK
32+
let newEmail = req.cookies["newEmail"];
3033
res.send('data is being processed')
3134
})

javascript/ql/test/query-tests/Security/CWE-352/lusca_example.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,26 @@ var app = express()
99
app.use(cookieParser())
1010

1111
app.post('/process', parseForm, lusca.csrf(), function (req, res) { // OK
12+
let newEmail = req.cookies["newEmail"];
1213
res.send('data is being processed')
1314
})
1415

1516
app.post('/process', parseForm, lusca({csrf:true}), function (req, res) { // OK
17+
let newEmail = req.cookies["newEmail"];
1618
res.send('data is being processed')
1719
})
1820

1921
app.post('/process', parseForm, lusca({csrf:{}}), function (req, res) { // OK
22+
let newEmail = req.cookies["newEmail"];
2023
res.send('data is being processed')
2124
})
2225

2326
app.post('/process', parseForm, lusca(), function (req, res) { // NOT OK - missing csrf option
27+
let newEmail = req.cookies["newEmail"];
2428
res.send('data is being processed')
2529
})
2630

2731
app.post('/process_unsafe', parseForm, function (req, res) { // NOT OK
32+
let newEmail = req.cookies["newEmail"];
2833
res.send('data is being processed')
2934
})
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
let express = require('express');
2+
let cookieParser = require('cookie-parser');
3+
4+
let app = express();
5+
6+
app.use(cookieParser());
7+
8+
app.post('/doSomethingTerrible', (req, res) => { // NOT OK - uses cookies
9+
if (req.cookies['secret'] === app.secret) {
10+
somethingTerrible();
11+
}
12+
res.end('Ok');
13+
});
14+
15+
app.post('/doSomethingElse', (req, res) => { // OK - doesn't actually use cookies
16+
somethingElse(req.query['data']);
17+
res.end('Ok');
18+
});
19+
20+
app.post('/doWithCaptcha', (req, res) => { // OK - attacker can't guess the captcha value either
21+
if (req.session['captcha'] !== req.query['captcha']) {
22+
res.end("You guessed wrong, that 'u' was actually a 'U'. Try again.");
23+
return;
24+
}
25+
somethingElse(req.query['data']);
26+
res.end('Ok');
27+
});
28+
29+
app.post('/user', (req, res) => { // NOT OK - access to req.user is unprotected
30+
somethingElse(req.user.name);
31+
res.end('Ok');
32+
});
33+
34+
app.post('/session', (req, res) => { // NOT OK - access to req.session is unprotected
35+
somethingElse(req.session.name);
36+
res.end('Ok');
37+
});
38+
39+
app.listen();

0 commit comments

Comments
 (0)