mirror of
https://github.com/github/codeql.git
synced 2026-04-28 10:15:14 +02:00
Cookies without HttpOnly
This commit is contained in:
@@ -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>
|
||||
@@ -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."
|
||||
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import InsecureCookie::Cookie
|
||||
import semmle.javascript.security.InsecureCookie::Cookie
|
||||
|
||||
from Cookie cookie
|
||||
where not cookie.isSecure()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user