Cookies without HttpOnly

This commit is contained in:
edvraa
2021-04-27 16:23:37 +03:00
parent 578ce1e512
commit 3aec9c1a41
14 changed files with 516 additions and 34 deletions

View File

@@ -0,0 +1,25 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Cookies without <code>HttpOnly</code> flag are accessible to JavaScript running in the same origin. In case of
Cross-Site Scripting (XSS) vulnerability the cookie can be stolen by malicious script.</p>
</overview>
<recommendation>
<p>Protect sensitive cookies, such as related to authentication, by setting <code>HttpOnly</code> to <code>true</code> to make
them not accessible to JavaScript.</p>
</recommendation>
<references>
<li>Production Best Practices: Security:<a href="https://expressjs.com/en/advanced/best-practice-security.html#use-cookies-securely">Use cookies securely</a>.</li>
<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>
<li>express-session:<a href="https://github.com/expressjs/session#cookiehttponly">cookie.httpOnly</a>.</li>
<li>cookie-session:<a href="https://github.com/expressjs/cookie-session#cookie-options">Cookie Options</a>.</li>
<li><a href="https://expressjs.com/en/api.html#res.cookie">express response.cookie</a>.</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie">Set-Cookie</a>.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,20 @@
/**
* @name 'HttpOnly' attribute is not set to true
* @description Omitting the 'HttpOnly' attribute for security sensitive data allows
* malicious JavaScript to steal it in case of XSS vulnerability. Always set
* 'HttpOnly' to 'true' to authentication related cookie to make it
* not accessible by JavaScript.
* @kind problem
* @problem.severity warning
* @precision high
* @id js/cookie-httponly-not-set
* @tags security
* external/cwe/cwe-1004
*/
import javascript
import semmle.javascript.security.InsecureCookie::Cookie
from Cookie cookie
where cookie.isAuthNotHttpOnly()
select cookie, "Cookie attribute 'HttpOnly' is not set to true."

View File

@@ -11,7 +11,7 @@
*/
import javascript
import InsecureCookie::Cookie
import semmle.javascript.security.InsecureCookie::Cookie
from Cookie cookie
where not cookie.isSecure()

View File

@@ -165,8 +165,11 @@ class InvokeNode extends DataFlow::SourceNode {
getOptionsArgument(i).hasPropertyWrite(name, result)
}
/**
* Holds if the `i`th argument of this invocation is an object literal set to `result`.
*/
pragma[noinline]
private ObjectLiteralNode getOptionsArgument(int i) { result.flowsTo(getArgument(i)) }
ObjectLiteralNode getOptionsArgument(int i) { result.flowsTo(getArgument(i)) }
/** Gets an abstract value representing possible callees of this call site. */
final AbstractValue getACalleeValue() {

View File

@@ -1,6 +1,7 @@
/**
* Provides classes for reasoning about cookies added to response without the 'secure' flag being set.
* A cookie without the 'secure' flag being set can be intercepted and read by a malicious user.
* A cookie without the 'httponly' flag being set can be read by an injected JavaScript
*/
import javascript
@@ -9,7 +10,12 @@ module Cookie {
/**
* `secure` property of the cookie options.
*/
string flag() { result = "secure" }
string secureFlag() { result = "secure" }
/**
* `httpOnly` property of the cookie options.
*/
string httpOnlyFlag() { result = "httpOnly" }
/**
* Abstract class to represent different cases of insecure cookie settings.
@@ -29,6 +35,38 @@ module Cookie {
* Holds if this cookie is secure.
*/
abstract predicate isSecure();
/**
* Holds if this cookie is HttpOnly.
*/
abstract predicate isHttpOnly();
/**
* Holds if the cookie is authentication sensitive and lacks HttpOnly.
*/
abstract predicate isAuthNotHttpOnly();
/**
* Holds if the expression is a variable with a sensitive name.
*/
predicate isAuthVariable(DataFlow::Node expr) {
exists(string val |
(
val = expr.getStringValue() or
val = expr.asExpr().(VarAccess).getName()
) and
regexpMatchAuth(val)
)
}
/**
* Holds if the string contains sensitive auth keyword, but not antiforgery token.
*/
bindingset[val]
predicate regexpMatchAuth(string val) {
val.regexpMatch("(?i).*(session|login|token|user|auth|credential).*") and
not val.regexpMatch("(?i).*(xsrf|csrf|forgery).*")
}
}
/**
@@ -37,7 +75,7 @@ module Cookie {
class InsecureCookieSession extends ExpressLibraries::CookieSession::MiddlewareInstance, Cookie {
override string getKind() { result = "cookie-session" }
override DataFlow::SourceNode getCookieOptionsArgument() { result = this.getOption("cookie") }
override DataFlow::SourceNode getCookieOptionsArgument() { result = this.getOptionsArgument(0) }
private DataFlow::Node getCookieFlagValue(string flag) {
result = this.getCookieOptionsArgument().getAPropertyWrite(flag).getRhs()
@@ -46,7 +84,17 @@ module Cookie {
override predicate isSecure() {
// The flag `secure` is set to `false` by default for HTTP, `true` by default for HTTPS (https://github.com/expressjs/cookie-session#cookie-options).
// A cookie is secure if the `secure` flag is not explicitly set to `false`.
not getCookieFlagValue(flag()).mayHaveBooleanValue(false)
not getCookieFlagValue(secureFlag()).mayHaveBooleanValue(false)
}
override predicate isAuthNotHttpOnly() {
not isHttpOnly() // It is a session cookie, likely auth sensitive
}
override predicate isHttpOnly() {
// The flag `httpOnly` is set to `true` by default (https://github.com/expressjs/cookie-session#cookie-options).
// A cookie is httpOnly if the `httpOnly` flag is not explicitly set to `false`.
not getCookieFlagValue(httpOnlyFlag()).mayHaveBooleanValue(false)
}
}
@@ -67,8 +115,19 @@ module Cookie {
// The flag `secure` is not set by default (https://github.com/expressjs/session#Cookieecure).
// The default value for cookie options is { path: '/', httpOnly: true, secure: false, maxAge: null }.
// A cookie is secure if there are the cookie options with the `secure` flag set to `true` or to `auto`.
getCookieFlagValue(flag()).mayHaveBooleanValue(true) or
getCookieFlagValue(flag()).mayHaveStringValue("auto")
getCookieFlagValue(secureFlag()).mayHaveBooleanValue(true) or
getCookieFlagValue(secureFlag()).mayHaveStringValue("auto")
}
override predicate isAuthNotHttpOnly() {
not isHttpOnly() // It is a session cookie, likely auth sensitive
}
override predicate isHttpOnly() {
// The flag `httpOnly` is set by default (https://github.com/expressjs/session#Cookieecure).
// The default value for cookie options is { path: '/', httpOnly: true, secure: false, maxAge: null }.
// A cookie is httpOnly if the `httpOnly` flag is not explicitly set to `false`.
not getCookieFlagValue(httpOnlyFlag()).mayHaveBooleanValue(false)
}
}
@@ -90,7 +149,19 @@ module Cookie {
override predicate isSecure() {
// A cookie is secure if there are cookie options with the `secure` flag set to `true`.
getCookieFlagValue(flag()).mayHaveBooleanValue(true)
// The default is `false`.
getCookieFlagValue(secureFlag()).mayHaveBooleanValue(true)
}
override predicate isAuthNotHttpOnly() {
isAuthVariable(this.getArgument(0)) and
not isHttpOnly()
}
override predicate isHttpOnly() {
// A cookie is httpOnly if there are cookie options with the `httpOnly` flag set to `true`.
// The default is `false`.
getCookieFlagValue(httpOnlyFlag()).mayHaveBooleanValue(true)
}
}
@@ -105,14 +176,42 @@ module Cookie {
override string getKind() { result = "set-cookie header" }
override DataFlow::Node getCookieOptionsArgument() {
result.asExpr() = this.asExpr().(ArrayExpr).getAnElement()
if this.asExpr() instanceof ArrayExpr
then result.asExpr() = this.asExpr().(ArrayExpr).getAnElement()
else result.asExpr() = this.asExpr()
}
override predicate isSecure() {
// A cookie is secure if the 'secure' flag is specified in the cookie definition.
exists(string s |
getCookieOptionsArgument().mayHaveStringValue(s) and
s.regexpMatch("(.*;)?\\s*secure.*")
// The default is `false`.
forall(DataFlow::Node n | n = getCookieOptionsArgument() |
exists(string s |
n.mayHaveStringValue(s) and
s.regexpMatch("(?i).*;\\s*secure\\s*;?.*$")
)
)
}
override predicate isAuthNotHttpOnly() {
exists(DataFlow::Node n | n = getCookieOptionsArgument() |
exists(string s |
n.mayHaveStringValue(s) and
(
not s.regexpMatch("(?i).*;\\s*httponly\\s*;?.*$") and
regexpMatchAuth(s.regexpCapture("\\s*([^=\\s]*)\\s*=.*", 1))
)
)
)
}
override predicate isHttpOnly() {
// A cookie is httpOnly if the 'httpOnly' flag is specified in the cookie definition.
// The default is `false`.
forall(DataFlow::Node n | n = getCookieOptionsArgument() |
exists(string s |
n.mayHaveStringValue(s) and
s.regexpMatch("(?i).*;\\s*httponly\\s*;?.*$")
)
)
}
}
@@ -142,7 +241,11 @@ module Cookie {
override predicate isSecure() {
// A cookie is secure if there are cookie options with the `secure` flag set to `true`.
getCookieFlagValue(flag()).mayHaveBooleanValue(true)
getCookieFlagValue(secureFlag()).mayHaveBooleanValue(true)
}
override predicate isAuthNotHttpOnly() { none() }
override predicate isHttpOnly() { none() } // js-cookie is browser side library and doesn't support HttpOnly
}
}

View File

@@ -0,0 +1,16 @@
| test_cookie-session.js:12:9:16:2 | session ... BAD\\n}) | Cookie attribute 'HttpOnly' is not set to true. |
| test_cookie-session.js:30:9:30:21 | session(sess) | Cookie attribute 'HttpOnly' is not set to true. |
| test_cookie-session.js:39:9:39:22 | session(sess2) | Cookie attribute 'HttpOnly' is not set to true. |
| test_cookie-session.js:48:9:48:22 | session(sess2) | Cookie attribute 'HttpOnly' is not set to true. |
| test_express-session.js:11:9:15:2 | session ... BAD\\n}) | Cookie attribute 'HttpOnly' is not set to true. |
| test_express-session.js:28:9:32:2 | session ... tter\\n}) | Cookie attribute 'HttpOnly' is not set to true. |
| test_httpserver.js:7:37:7:48 | "auth=ninja" | Cookie attribute 'HttpOnly' is not set to true. |
| test_httpserver.js:27:37:27:70 | ["auth= ... cript"] | Cookie attribute 'HttpOnly' is not set to true. |
| test_httpserver.js:57:37:57:80 | ["auth= ... cript"] | Cookie attribute 'HttpOnly' is not set to true. |
| test_responseCookie.js:15:5:20:10 | res.coo ... }) | Cookie attribute 'HttpOnly' is not set to true. |
| test_responseCookie.js:25:5:28:10 | res.coo ... }) | Cookie attribute 'HttpOnly' is not set to true. |
| test_responseCookie.js:48:5:48:43 | res.coo ... ptions) | Cookie attribute 'HttpOnly' is not set to true. |
| test_responseCookie.js:56:5:56:43 | res.coo ... ptions) | Cookie attribute 'HttpOnly' is not set to true. |
| test_responseCookie.js:65:5:65:43 | res.coo ... ptions) | Cookie attribute 'HttpOnly' is not set to true. |
| test_responseCookie.js:84:5:84:43 | res.coo ... ptions) | Cookie attribute 'HttpOnly' is not set to true. |
| test_responseCookie.js:95:5:95:41 | res.coo ... ptions) | Cookie attribute 'HttpOnly' is not set to true. |

View File

@@ -0,0 +1 @@
experimental/Security/CWE-1004/CookieWithoutHttpOnly.ql

View File

@@ -0,0 +1,48 @@
const express = require('express')
const app = express()
const session = require('cookie-session')
const expiryDate = new Date(Date.now() + 60 * 60 * 1000)
app.use(session({
name: 'session',
keys: ['key1', 'key2'],
httpOnly: true, // GOOD
}))
app.use(session({
name: 'session',
keys: ['key1', 'key2'],
httpOnly: false // BAD
}))
app.use(session({
name: 'session',
keys: ['key1', 'key2'],
secure: true // GOOD, httpOnly is true by default
}))
var sess = {
name: 'session',
keys: ['key1', 'key2'],
}
sess.httpOnly = false;
app.use(session(sess)) // BAD
var sess2 = {
name: 'session',
keys: ['key1', 'key2'],
httpOnly: true,
}
sess2.httpOnly = false;
app.use(session(sess2)) // BAD
var sess2 = {
name: 'mycookie',
keys: ['key1', 'key2'],
httpOnly: true,
}
sess2.httpOnly = false;
app.use(session(sess2)) // BAD, It is a session cookie, name doesn't matter

View File

@@ -0,0 +1,32 @@
const express = require('express')
const app = express()
const session = require('express-session')
app.use(session({
name: 'session',
keys: ['key1', 'key2'],
cookie: { httpOnly: true }, // GOOD
}))
app.use(session({
name: 'session',
keys: ['key1', 'key2'],
cookie: { httpOnly: false } // BAD
}))
app.use(session({
name: 'session',
keys: ['key1', 'key2'],
cookie: { secure: true } // GOOD, httpOnly is true by default
}))
app.use(session({ // GOOD, httpOnly is true by default
name: 'session',
keys: ['key1', 'key2']
}))
app.use(session({
name: 'mycookie',
keys: ['key1', 'key2'],
cookie: { httpOnly: false } // BAD, It is a session cookie, name doesn't matter
}))

View File

@@ -0,0 +1,71 @@
const http = require('http');
function test1() {
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
// BAD
res.setHeader("Set-Cookie", "auth=ninja");
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
}
function test2() {
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
// GOOD
res.setHeader("Set-Cookie", "auth=ninja; HttpOnly");
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
}
function test3() {
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
// BAD
res.setHeader("Set-Cookie", ["auth=ninja", "token=javascript"]);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
}
function test4() {
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
// GOOD
res.setHeader("Set-Cookie", ["auth=ninja; HttpOnly"]);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
}
function test5() {
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
// GOOD, case insensitive
res.setHeader("Set-Cookie", ["auth=ninja; httponly"]);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
}
function test6() {
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
// BAD
res.setHeader("Set-Cookie", ["auth=ninja; httponly", "token=javascript"]);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
}
function test7() {
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
// Good, not auth related
res.setHeader("Set-Cookie", ["foo=ninja", "bar=javascript"]);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
}

View File

@@ -0,0 +1,126 @@
const express = require('express')
const app = express()
app.get('/a', function (req, res, next) {
res.cookie('session', 'value',
{
maxAge: 9000000000,
httpOnly: true, // GOOD
secure: false
});
res.end('ok')
})
app.get('/a', function (req, res, next) {
res.cookie('session', 'value',
{
maxAge: 9000000000,
httpOnly: false, // BAD
secure: false
});
res.end('ok')
})
app.get('/a', function (req, res, next) {
res.cookie('session', 'value',
{
maxAge: 9000000000
});
res.end('ok') // BAD
})
app.get('/a', function (req, res, next) {
let options = {
maxAge: 9000000000,
httpOnly: true, // GOOD
secure: false
}
res.cookie('session', 'value', options);
res.end('ok')
})
app.get('/a', function (req, res, next) {
let options = {
maxAge: 9000000000,
httpOnly: false, // BAD
secure: false
}
res.cookie('session', 'value', options);
res.end('ok')
})
app.get('/a', function (req, res, next) {
let options = {
maxAge: 9000000000
}
res.cookie('session', 'value', options); // BAD
res.end('ok')
})
app.get('/a', function (req, res, next) {
let options = {
maxAge: 9000000000
}
options.httpOnly = false;
res.cookie('session', 'value', options); // BAD
res.end('ok')
})
app.get('/a', function (req, res, next) {
let options = {
maxAge: 9000000000
}
options.httpOnly = true;
res.cookie('session', 'value', options); // GOOD
res.end('ok')
})
app.get('/a', function (req, res, next) {
let options = {
maxAge: 9000000000,
httpOnly: false,
}
options.httpOnly = false;
res.cookie('session', 'value', options); // BAD
res.end('ok')
})
app.get('/a', function (req, res, next) {
let options = {
maxAge: 9000000000,
httpOnly: false,
}
options.httpOnly = false;
let session = "blabla"
res.cookie(session, 'value', options); // BAD, var name likely auth related
res.end('ok')
})
app.get('/a', function (req, res, next) {
let options = {
maxAge: 9000000000,
httpOnly: true,
}
options.httpOnly = true;
res.cookie('session', 'value', options); // GOOD
res.end('ok')
})
app.get('/a', function (req, res, next) {
let options = {
maxAge: 9000000000,
httpOnly: false,
}
options.httpOnly = true;
res.cookie('session', 'value', options); // GOOD
res.end('ok')
})
app.get('/a', function (req, res, next) {
let options = {
maxAge: 9000000000,
httpOnly: false,
}
res.cookie('mycookie', 'value', options); // GOOD, name likely is not auth sensitive
res.end('ok')
})

View File

@@ -1,9 +1,11 @@
| test_cookie-session.js:18:9:28:2 | session ... }\\n}) | Cookie is added to response without the 'secure' flag being set to true |
| test_cookie-session.js:16:9:24:2 | session ... Date\\n}) | Cookie is added to response without the 'secure' flag being set to true |
| 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 |
| 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 |
| 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 |
| test_express-session.js:25:9:25:21 | session(sess) | Cookie is added to response without the 'secure' flag being set to true |
| test_httpserver.js:7:37:7:73 | ["type= ... cript"] | Cookie is added to response without the 'secure' flag being set to true |
| test_httpserver.js:7:37:7:48 | "type=ninja" | Cookie is added to response without the 'secure' flag being set to true |
| test_httpserver.js:27:37:27:73 | ["type= ... cript"] | Cookie is added to response without the 'secure' flag being set to true |
| test_httpserver.js:57:37:57:81 | ["type= ... cript"] | Cookie is added to response without the 'secure' flag being set to true |
| test_jscookie.js:2:1:2:48 | js_cook ... alse }) | Cookie is added to response without the 'secure' flag being set to true |
| test_responseCookie.js:5:5:10:10 | res.coo ... }) | Cookie is added to response without the 'secure' flag being set to true |
| test_responseCookie.js:20:5:20:40 | res.coo ... ptions) | Cookie is added to response without the 'secure' flag being set to true |

View File

@@ -6,23 +6,19 @@ const expiryDate = new Date(Date.now() + 60 * 60 * 1000)
app.use(session({
name: 'session',
keys: ['key1', 'key2'],
cookie: {
secure: true, // OK
httpOnly: true,
domain: 'example.com',
path: 'foo/bar',
expires: expiryDate
}
secure: true, // OK
httpOnly: true,
domain: 'example.com',
path: 'foo/bar',
expires: expiryDate
}))
app.use(session({
name: 'session',
keys: ['key1', 'key2'],
cookie: {
secure: false, // NOT OK
httpOnly: true,
domain: 'example.com',
path: 'foo/bar',
expires: expiryDate
}
secure: false, // NOT OK
httpOnly: true,
domain: 'example.com',
path: 'foo/bar',
expires: expiryDate
}))

View File

@@ -3,20 +3,59 @@ const http = require('http');
function test1() {
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
// NOT OK
// BAD
res.setHeader("Set-Cookie", "type=ninja");
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
}
function test2() {
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
// GOOD
res.setHeader("Set-Cookie", "type=ninja; Secure");
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
}
function test3() {
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
// BAD
res.setHeader("Set-Cookie", ["type=ninja", "language=javascript"]);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
}
function test2() {
function test4() {
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
// OK
res.setHeader("Set-Cookie", ["type=ninja; Secure", "language=javascript; secure"]);
// GOOD
res.setHeader("Set-Cookie", ["type=ninja; Secure"]);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
}
function test5() {
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
// GOOD, case insensitive
res.setHeader("Set-Cookie", ["type=ninja; secure"]);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
}
function test6() {
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
// BAD
res.setHeader("Set-Cookie", ["type=ninja; secure", "language=javascript"]);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
}