mirror of
https://github.com/github/codeql.git
synced 2025-12-29 23:26:34 +01:00
428 lines
15 KiB
Plaintext
428 lines
15 KiB
Plaintext
/**
|
|
* Provides classes for reasoning about cookies.
|
|
*/
|
|
|
|
import javascript
|
|
|
|
/**
|
|
* Classes and predicates for reasoning about writes to cookies.
|
|
*/
|
|
module CookieWrites {
|
|
/**
|
|
* A write to a cookie.
|
|
*/
|
|
abstract class CookieWrite extends DataFlow::Node {
|
|
/**
|
|
* Holds if this cookie is secure, i.e. only transmitted over SSL.
|
|
*/
|
|
abstract predicate isSecure();
|
|
|
|
/**
|
|
* Holds if this cookie is HttpOnly, i.e. not accessible by JavaScript.
|
|
*/
|
|
abstract predicate isHttpOnly();
|
|
|
|
/**
|
|
* Holds if the cookie likely is an authentication cookie or otherwise sensitive.
|
|
*/
|
|
abstract predicate isSensitive();
|
|
|
|
/**
|
|
* Gets the SameSite attribute of the cookie if present.
|
|
* Either "Strict", "Lax" or "None".
|
|
*/
|
|
abstract string getSameSite();
|
|
|
|
/**
|
|
* Holds if the cookie write happens on a server, i.e. the `httpOnly` flag is relevant.
|
|
*/
|
|
predicate isServerSide() {
|
|
any() // holds by default. Client-side cookie writes should extend ClientSideCookieWrite.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A client-side write to a cookie.
|
|
*/
|
|
abstract class ClientSideCookieWrite extends CookieWrite {
|
|
final override predicate isHttpOnly() { none() }
|
|
|
|
final override predicate isServerSide() { none() }
|
|
}
|
|
|
|
/**
|
|
* Gets the flag that indicates that a cookie is secure.
|
|
*/
|
|
string secure() { result = "secure" }
|
|
|
|
/**
|
|
* Gets the flag that indicates that a cookie is HttpOnly.
|
|
*/
|
|
string httpOnly() { result = "httpOnly" }
|
|
}
|
|
|
|
/**
|
|
* Holds if `node` looks like it can contain a sensitive cookie.
|
|
*
|
|
* Heuristics:
|
|
* - `node` contains a string value that looks like a sensitive cookie name
|
|
* - `node` is a sensitive expression
|
|
*/
|
|
private predicate canHaveSensitiveCookie(DataFlow::Node node) {
|
|
exists(string s |
|
|
node.mayHaveStringValue(s) or
|
|
s = node.(StringOps::ConcatenationRoot).getConstantStringParts()
|
|
|
|
|
HeuristicNames::nameIndicatesSensitiveData([s, getCookieName(s)], _)
|
|
)
|
|
or
|
|
node instanceof SensitiveNode
|
|
}
|
|
|
|
/**
|
|
* Gets the cookie name of a `Set-Cookie` header value.
|
|
* The header value always starts with `<cookie-name>=<cookie-value>` optionally followed by attributes:
|
|
* `<cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly`
|
|
*/
|
|
bindingset[s]
|
|
private string getCookieName(string s) { result = s.regexpCapture("([^=]*)=.*", 1).trim() }
|
|
|
|
/**
|
|
* Holds if the `Set-Cookie` header value contains the specified attribute
|
|
* 1. The attribute is case insensitive
|
|
* 2. It always starts with a pair `<cookie-name>=<cookie-value>`.
|
|
* If the attribute is present there must be `;` after the pair.
|
|
* Other attributes like `Domain=`, `Path=`, etc. may come after the pair:
|
|
* `<cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly`
|
|
* See `https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie`
|
|
*/
|
|
bindingset[s, attribute]
|
|
private predicate hasCookieAttribute(string s, string attribute) {
|
|
s.regexpMatch("(?i).*;\\s*" + attribute + "\\b\\s*;?.*$")
|
|
}
|
|
|
|
/**
|
|
* Gets the value for a `Set-Cookie` header attribute.
|
|
*/
|
|
bindingset[s, attribute]
|
|
private string getCookieValue(string s, string attribute) {
|
|
result = s.regexpCapture("(?i).*;\\s*" + attribute + "=(\\w+)\\b\\s*;?.*$", 1)
|
|
}
|
|
|
|
/**
|
|
* Gets the "SameSite" value for a given `node`.
|
|
* Converts boolean values to the corresponding string value.
|
|
*
|
|
* Not all libraries support boolean values for the `SameSite` attribute,
|
|
* but here we assume that they do.
|
|
*/
|
|
private string getSameSiteValue(DataFlow::Node node) {
|
|
node.mayHaveStringValue(result)
|
|
or
|
|
node.mayHaveBooleanValue(true) and
|
|
result = "Strict"
|
|
or
|
|
node.mayHaveBooleanValue(false) and
|
|
result = "Lax"
|
|
}
|
|
|
|
/**
|
|
* A model of the `js-cookie` library (https://github.com/js-cookie/js-cookie).
|
|
*/
|
|
private module JsCookie {
|
|
/**
|
|
* Gets a function call that invokes method `name` of the `js-cookie` library.
|
|
*/
|
|
DataFlow::CallNode libMemberCall(string name) {
|
|
result = DataFlow::globalVarRef("Cookie").getAMemberCall(name) or
|
|
result = DataFlow::globalVarRef("Cookie").getAMemberCall("noConflict").getAMemberCall(name) or
|
|
result = DataFlow::moduleMember("js-cookie", name).getACall() or
|
|
// es-cookie behaves basically the same as js-cookie
|
|
result = DataFlow::moduleMember("es-cookie", name).getACall()
|
|
}
|
|
|
|
class ReadAccess extends PersistentReadAccess, DataFlow::CallNode {
|
|
ReadAccess() { this = libMemberCall("get") }
|
|
|
|
override PersistentWriteAccess getAWrite() {
|
|
this.getArgument(0).mayHaveStringValue(result.(WriteAccess).getKey())
|
|
}
|
|
}
|
|
|
|
class WriteAccess extends PersistentWriteAccess, DataFlow::CallNode,
|
|
CookieWrites::ClientSideCookieWrite {
|
|
WriteAccess() { this = libMemberCall("set") }
|
|
|
|
string getKey() { this.getArgument(0).mayHaveStringValue(result) }
|
|
|
|
override DataFlow::Node getValue() { result = this.getArgument(1) }
|
|
|
|
override predicate isSecure() {
|
|
// A cookie is secure if there are cookie options with the `secure` flag set to `true`.
|
|
exists(DataFlow::Node value | value = this.getOptionArgument(2, CookieWrites::secure()) |
|
|
not value.mayHaveBooleanValue(false) // anything but `false` is accepted as being maybe true
|
|
)
|
|
}
|
|
|
|
override predicate isSensitive() { canHaveSensitiveCookie(this.getArgument(0)) }
|
|
|
|
override string getSameSite() {
|
|
result = getSameSiteValue(this.getOptionArgument(2, "sameSite"))
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A model of the `browser-cookies` library (https://github.com/voltace/browser-cookies).
|
|
*/
|
|
private module BrowserCookies {
|
|
/**
|
|
* Gets a function call that invokes method `name` of the `browser-cookies` library.
|
|
*/
|
|
DataFlow::CallNode libMemberCall(string name) {
|
|
result = DataFlow::moduleMember("browser-cookies", name).getACall()
|
|
}
|
|
|
|
class ReadAccess extends PersistentReadAccess, DataFlow::CallNode {
|
|
ReadAccess() { this = libMemberCall("get") }
|
|
|
|
override PersistentWriteAccess getAWrite() {
|
|
this.getArgument(0).mayHaveStringValue(result.(WriteAccess).getKey())
|
|
}
|
|
}
|
|
|
|
class WriteAccess extends PersistentWriteAccess, DataFlow::CallNode,
|
|
CookieWrites::ClientSideCookieWrite {
|
|
WriteAccess() { this = libMemberCall("set") }
|
|
|
|
string getKey() { this.getArgument(0).mayHaveStringValue(result) }
|
|
|
|
override DataFlow::Node getValue() { result = this.getArgument(1) }
|
|
|
|
override predicate isSecure() {
|
|
// A cookie is secure if there are cookie options with the `secure` flag set to `true`.
|
|
exists(DataFlow::Node value | value = this.getOptionArgument(2, CookieWrites::secure()) |
|
|
not value.mayHaveBooleanValue(false) // anything but `false` is accepted as being maybe true
|
|
)
|
|
or
|
|
// or, an explicit default has been set
|
|
exists(DataFlow::moduleMember("browser-cookies", "defaults").getAPropertyWrite("secure"))
|
|
}
|
|
|
|
override predicate isSensitive() { canHaveSensitiveCookie(this.getArgument(0)) }
|
|
|
|
override string getSameSite() {
|
|
result = getSameSiteValue(this.getOptionArgument(2, "samesite"))
|
|
or
|
|
// or, an explicit default has been set
|
|
DataFlow::moduleMember("browser-cookies", "defaults")
|
|
.getAPropertyWrite("samesite")
|
|
.mayHaveStringValue(result)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A model of the `cookie` library (https://github.com/jshttp/cookie).
|
|
*/
|
|
private module LibCookie {
|
|
/**
|
|
* Gets a function call that invokes method `name` of the `cookie` library.
|
|
*/
|
|
DataFlow::CallNode libMemberCall(string name) {
|
|
result = DataFlow::moduleMember("cookie", name).getACall()
|
|
}
|
|
|
|
class ReadAccess extends PersistentReadAccess {
|
|
string key;
|
|
|
|
ReadAccess() { this = libMemberCall("parse").getAPropertyRead(key) }
|
|
|
|
override PersistentWriteAccess getAWrite() { key = result.(WriteAccess).getKey() }
|
|
}
|
|
|
|
class WriteAccess extends PersistentWriteAccess, DataFlow::CallNode,
|
|
CookieWrites::ClientSideCookieWrite {
|
|
WriteAccess() { this = libMemberCall("serialize") }
|
|
|
|
string getKey() { this.getArgument(0).mayHaveStringValue(result) }
|
|
|
|
override DataFlow::Node getValue() { result = this.getArgument(1) }
|
|
|
|
override predicate isSecure() {
|
|
// A cookie is secure if there are cookie options with the `secure` flag set to `true`.
|
|
exists(DataFlow::Node value | value = this.getOptionArgument(2, CookieWrites::secure()) |
|
|
not value.mayHaveBooleanValue(false) // anything but `false` is accepted as being maybe true
|
|
)
|
|
}
|
|
|
|
override predicate isSensitive() { canHaveSensitiveCookie(this.getArgument(0)) }
|
|
|
|
override string getSameSite() {
|
|
result = getSameSiteValue(this.getOptionArgument(2, "sameSite"))
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A model of cookies in an express application.
|
|
*/
|
|
private module ExpressCookies {
|
|
/**
|
|
* A cookie set using `response.cookie` from `express` module (https://expressjs.com/en/api.html#res.cookie).
|
|
*/
|
|
private class InsecureExpressCookieResponse extends CookieWrites::CookieWrite instanceof Express::SetCookie {
|
|
override predicate isSecure() {
|
|
// A cookie is secure if there are cookie options with the `secure` flag set to `true`.
|
|
// The default is `false`.
|
|
exists(DataFlow::Node value | value = super.getOptionArgument(2, CookieWrites::secure()) |
|
|
not value.mayHaveBooleanValue(false) // anything but `false` is accepted as being maybe true
|
|
)
|
|
}
|
|
|
|
override predicate isSensitive() { canHaveSensitiveCookie(super.getArgument(0)) }
|
|
|
|
override predicate isHttpOnly() {
|
|
// A cookie is httpOnly if there are cookie options with the `httpOnly` flag set to `true`.
|
|
// The default is `false`.
|
|
exists(DataFlow::Node value | value = super.getOptionArgument(2, CookieWrites::httpOnly()) |
|
|
not value.mayHaveBooleanValue(false) // anything but `false` is accepted as being maybe true
|
|
)
|
|
}
|
|
|
|
override string getSameSite() {
|
|
result = getSameSiteValue(super.getOptionArgument(2, "sameSite"))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A cookie set using the `express` module `cookie-session` (https://github.com/expressjs/cookie-session).
|
|
*/
|
|
class InsecureCookieSession extends ExpressLibraries::CookieSession::MiddlewareInstance,
|
|
CookieWrites::CookieWrite {
|
|
private DataFlow::Node getCookieFlagValue(string flag) {
|
|
result = this.getOptionArgument(0, flag)
|
|
}
|
|
|
|
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 this.getCookieFlagValue(CookieWrites::secure()).mayHaveBooleanValue(false)
|
|
}
|
|
|
|
override predicate isSensitive() {
|
|
any() // 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 this.getCookieFlagValue(CookieWrites::httpOnly()).mayHaveBooleanValue(false)
|
|
}
|
|
|
|
override string getSameSite() { result = getSameSiteValue(this.getCookieFlagValue("sameSite")) }
|
|
}
|
|
|
|
/**
|
|
* A cookie set using the `express` module `express-session` (https://github.com/expressjs/session).
|
|
*/
|
|
class InsecureExpressSessionCookie extends ExpressLibraries::ExpressSession::MiddlewareInstance,
|
|
CookieWrites::CookieWrite {
|
|
private DataFlow::Node getCookieFlagValue(string flag) {
|
|
result = this.getOption("cookie").getALocalSource().getAPropertyWrite(flag).getRhs()
|
|
}
|
|
|
|
override predicate isSecure() {
|
|
// The flag `secure` is not set by default (https://github.com/expressjs/session#Cookiesecure).
|
|
// The default value for cookie options is { path: '/', httpOnly: true, secure: false, maxAge: null }.
|
|
exists(DataFlow::Node value | value = this.getCookieFlagValue(CookieWrites::secure()) |
|
|
not value.mayHaveBooleanValue(false) // anything but `false` is accepted as being maybe true
|
|
)
|
|
}
|
|
|
|
override predicate isSensitive() {
|
|
any() // It is a session cookie, likely auth sensitive
|
|
}
|
|
|
|
override predicate isHttpOnly() {
|
|
// The flag `httpOnly` is set by default (https://github.com/expressjs/session#Cookiesecure).
|
|
// 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 this.getCookieFlagValue(CookieWrites::httpOnly()).mayHaveBooleanValue(false)
|
|
}
|
|
|
|
override string getSameSite() { result = getSameSiteValue(this.getCookieFlagValue("sameSite")) }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A cookie set using `Set-Cookie` header of an `HTTP` response, where a raw header is used.
|
|
* (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).
|
|
* This class does not model the Express implementation of `HTTP::CookieDefintion`
|
|
* as the express implementation does not use raw headers.
|
|
*
|
|
* In case an array is passed `setHeader("Set-Cookie", [...]` it sets multiple cookies.
|
|
* We model a `CookieWrite` for each array element.
|
|
*/
|
|
private class HttpCookieWrite extends CookieWrites::CookieWrite {
|
|
string header;
|
|
|
|
HttpCookieWrite() {
|
|
exists(Http::CookieDefinition setCookie |
|
|
this = setCookie.getHeaderArgument() and
|
|
not this instanceof DataFlow::ArrayCreationNode
|
|
or
|
|
this = setCookie.getHeaderArgument().(DataFlow::ArrayCreationNode).getAnElement()
|
|
) and
|
|
header =
|
|
[
|
|
any(string s | this.mayHaveStringValue(s)),
|
|
this.(StringOps::ConcatenationRoot).getConstantStringParts()
|
|
]
|
|
}
|
|
|
|
override predicate isSecure() {
|
|
// A cookie is secure if the `secure` flag is specified in the cookie definition.
|
|
// The default is `false`.
|
|
hasCookieAttribute(header, CookieWrites::secure())
|
|
}
|
|
|
|
override predicate isHttpOnly() {
|
|
// A cookie is httpOnly if the `httpOnly` flag is specified in the cookie definition.
|
|
// The default is `false`.
|
|
hasCookieAttribute(header, CookieWrites::httpOnly())
|
|
}
|
|
|
|
override predicate isSensitive() { canHaveSensitiveCookie(this) }
|
|
|
|
override string getSameSite() { result = getCookieValue(header, "SameSite") }
|
|
}
|
|
|
|
/**
|
|
* A write to `document.cookie`.
|
|
*/
|
|
private class DocumentCookieWrite extends CookieWrites::ClientSideCookieWrite {
|
|
string cookie;
|
|
DataFlow::PropWrite write;
|
|
|
|
DocumentCookieWrite() {
|
|
this = write and
|
|
write = DOM::documentRef().getAPropertyWrite("cookie") and
|
|
cookie =
|
|
[
|
|
any(string s | write.getRhs().mayHaveStringValue(s)),
|
|
write.getRhs().(StringOps::ConcatenationRoot).getConstantStringParts()
|
|
]
|
|
}
|
|
|
|
override predicate isSecure() {
|
|
// A cookie is secure if the `secure` flag is specified in the cookie definition.
|
|
// The default is `false`.
|
|
hasCookieAttribute(cookie, CookieWrites::secure())
|
|
}
|
|
|
|
override predicate isSensitive() { canHaveSensitiveCookie(write.getRhs()) }
|
|
|
|
override string getSameSite() { result = getCookieValue(cookie, "SameSite") }
|
|
}
|