Merge branch 'main' into protoLib

This commit is contained in:
Erik Krogh Kristensen
2021-11-04 12:53:53 +01:00
387 changed files with 10210 additions and 5071 deletions

View File

@@ -12,7 +12,7 @@
import javascript
/** Holds if `base` declares or inherits method `m` with the given `name`. */
predicate hasMethod(ClassDefinition base, string name, MethodDefinition m) {
predicate hasMethod(ClassDefinition base, string name, MethodDeclaration m) {
m = base.getMethod(name) or
hasMethod(base.getSuperClassDefinition(), name, m)
}
@@ -22,7 +22,7 @@ predicate hasMethod(ClassDefinition base, string name, MethodDefinition m) {
* where `fromMethod` and `toMethod` are of kind `fromKind` and `toKind`, respectively.
*/
predicate isLocalMethodAccess(
PropAccess access, MethodDefinition fromMethod, string fromKind, MethodDefinition toMethod,
PropAccess access, MethodDefinition fromMethod, string fromKind, MethodDeclaration toMethod,
string toKind
) {
hasMethod(fromMethod.getDeclaringClass(), access.getPropertyName(), toMethod) and
@@ -32,7 +32,7 @@ predicate isLocalMethodAccess(
toKind = getKind(toMethod)
}
string getKind(MethodDefinition m) {
string getKind(MethodDeclaration m) {
if m.isStatic() then result = "static" else result = "instance"
}

View File

@@ -9,6 +9,8 @@
* @id js/sql-injection
* @tags security
* external/cwe/cwe-089
* external/cwe/cwe-090
* external/cwe/cwe-943
*/
import javascript

View File

@@ -0,0 +1,40 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Authentication cookies stored by a server can be accessed by a client if the <code>httpOnly</code> flag is not set.
</p>
<p>
An attacker that manages a cross-site scripting (XSS) attack can read the cookie and hijack the session.
</p>
</overview>
<recommendation>
<p>
Set the <code>httpOnly</code> flag on all cookies that are not needed by the client.
</p>
</recommendation>
<example>
<p>
The following example stores an authentication token in a cookie that can
be viewed by the client.
</p>
<sample src="examples/ClientExposedCookieGood.js"/>
<p>
To force the cookie to be transmitted using SSL, set the <code>secure</code>
attribute on the cookie.
</p>
<sample src="examples/ClientExposedCookieBad.js"/>
</example>
<references>
<li>ExpressJS: <a href="https://expressjs.com/en/advanced/best-practice-security.html#use-cookies-securely">Use cookies securely</a>.</li>
<li>OWASP: <a href="https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html#set-cookie-flags-appropriately">Set cookie flags appropriately</a>.</li>
<li>Mozilla: <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 Sensitive server cookie exposed to the client
* @description Sensitive cookies set by a server can be read by the client if the `httpOnly` flag is not set.
* @kind problem
* @problem.severity warning
* @security-severity 5.0
* @precision high
* @id js/client-exposed-cookie
* @tags security
* external/cwe/cwe-1004
*/
import javascript
from CookieWrites::CookieWrite cookie
where
cookie.isSensitive() and
cookie.isServerSide() and
not cookie.isHttpOnly()
select cookie, "Sensitive server cookie is missing 'httpOnly' flag."

View File

@@ -0,0 +1,7 @@
const http = require('http');
const server = http.createServer((req, res) => {
res.setHeader("Set-Cookie", `authKey=${makeAuthkey()}`);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h2>Hello world</h2>');
});

View File

@@ -0,0 +1,7 @@
const http = require('http');
const server = http.createServer((req, res) => {
res.setHeader("Set-Cookie", `authKey=${makeAuthkey()}; secure; httpOnly`);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h2>Hello world</h2>');
});

View File

@@ -10,6 +10,7 @@
* @id js/stack-trace-exposure
* @tags security
* external/cwe/cwe-209
* external/cwe/cwe-497
*/
import javascript

View File

@@ -0,0 +1,38 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Cookies that are transmitted in clear text can be intercepted by an attacker.
If sensitive cookies are intercepted, the attacker can read the cookie and
use it to perform actions on the user's behalf.
</p>
</overview>
<recommendation>
<p>
Always transmit sensitive cookies using SSL by setting the <code>secure</code>
attribute on the cookie.
</p>
</recommendation>
<example>
<p>
The following example stores an authentication token in a cookie that can
be transmitted in clear text.
</p>
<sample src="examples/ClearTextCookieBad.js"/>
<p>
To force the cookie to be transmitted using SSL, set the <code>secure</code>
attribute on the cookie.
</p>
<sample src="examples/ClearTextCookieGood.js"/>
</example>
<references>
<li>ExpressJS: <a href="https://expressjs.com/en/advanced/best-practice-security.html#use-cookies-securely">Use cookies securely</a>.</li>
<li>OWASP: <a href="https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html#set-cookie-flags-appropriately">Set cookie flags appropriately</a>.</li>
<li>Mozilla: <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,21 @@
/**
* @name Clear text transmission of sensitive cookie
* @description Sending sensitive information in a cookie without requring SSL encryption
* can expose the cookie to an attacker.
* @kind problem
* @problem.severity warning
* @security-severity 5.0
* @precision high
* @id js/clear-text-cookie
* @tags security
* external/cwe/cwe-614
* external/cwe/cwe-311
* external/cwe/cwe-312
* external/cwe/cwe-319
*/
import javascript
from CookieWrites::CookieWrite cookie
where cookie.isSensitive() and not cookie.isSecure()
select cookie, "Sensitive cookie sent without enforcing SSL encryption"

View File

@@ -0,0 +1,7 @@
const http = require('http');
const server = http.createServer((req, res) => {
res.setHeader("Set-Cookie", `authKey=${makeAuthkey()}`);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h2>Hello world</h2>');
});

View File

@@ -0,0 +1,7 @@
const http = require('http');
const server = http.createServer((req, res) => {
res.setHeader("Set-Cookie", `authKey=${makeAuthkey()}; secure; httpOnly`);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h2>Hello world</h2>');
});

View File

@@ -1,50 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>If an LDAP query is built using string concatenation or string formatting, and the
components of the concatenation include user input without any proper sanitization, a user
is likely to be able to run malicious LDAP queries.</p>
</overview>
<recommendation>
<p>If user input must be included in an LDAP query, it should be escaped to
avoid a malicious user providing special characters that change the meaning
of the query. In NodeJS, it is possible to build the LDAP query using frameworks like <code>ldapjs</code>.
The library provides a <code>Filter API</code>, however it's still possibile to pass a string version of an LDAP filter.
A good practice is to escape filter characters that could change the meaning of the query (https://tools.ietf.org/search/rfc4515#section-3).</p>
</recommendation>
<example>
<p>In the following examples, the code accepts a <code>username</code> from the user, which it uses in a LDAP query.</p>
<p>The first and the second example uses the unsanitized user input directly
in the search filter for the LDAP query.
A malicious user could provide special characters to change the meaning of these
queries, and search for a completely different set of values.
</p>
<sample src="examples/example_bad1.js" />
<sample src="examples/example_bad2.js" />
<p>In the third example the <code>username</code> is sanitized before it is included in the search filters.
This ensures the meaning of the query cannot be changed by a malicious user.</p>
<sample src="examples/example_good1.js" />
<p>In the fourth example the <code>username</code> is passed to an <code>OrFilter</code> filter before it is included in the search filters.
This ensures the meaning of the query cannot be changed by a malicious user.</p>
<sample src="examples/example_good2.js" />
</example>
<references>
<li>OWASP: <a href="https://cheatsheetseries.owasp.org/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.html">LDAP Injection Prevention Cheat Sheet</a>.</li>
<li>LDAPjs: <a href="http://ldapjs.org/index.html">Documentation for LDAPjs</a>.</li>
<li>Github: <a href="https://github.com/ldapjs/node-ldapjs">ldapjs</a>.</li>
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/LDAP_injection">LDAP injection</a>.</li>
<li>BlackHat: <a href="https://www.blackhat.com/presentations/bh-europe-08/Alonso-Parada/Whitepaper/bh-eu-08-alonso-parada-WP.pdf">LDAP Injection and Blind LDAP Injection</a>.</li>
<li>LDAP: <a href="https://ldap.com/2018/05/04/understanding-and-defending-against-ldap-injection-attacks/">Understanding and Defending Against LDAP Injection Attacks</a>.</li>
</references>
</qhelp>

View File

@@ -1,20 +0,0 @@
/**
* @name LDAP query built from user-controlled sources
* @description Building an LDAP query from user-controlled sources is vulnerable to insertion of
* malicious LDAP code by the user.
* @kind path-problem
* @problem.severity error
* @precision high
* @id javascript/ldap-injection
* @tags security
* external/cwe/cwe-090
*/
import javascript
import DataFlow::PathGraph
import LdapInjection::LdapInjection
from LdapInjectionConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ might include code from $@.",
sink.getNode().(Sink).getQueryCall(), "LDAP query call", source.getNode(), "user-provided value"

View File

@@ -1,25 +0,0 @@
import javascript
module LdapInjection {
import LdapInjectionCustomizations::LdapInjection
/**
* A taint-tracking configuration for reasoning about LDAP injection vulnerabilities.
*/
class LdapInjectionConfiguration extends TaintTracking::Configuration {
LdapInjectionConfiguration() { this = "LdapInjection" }
override predicate isSource(DataFlow::Node source) { source instanceof Source }
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(LdapjsParseFilter filter |
pred = filter.getArgument(0) and
succ = filter
)
}
}
}

View File

@@ -1,73 +0,0 @@
/**
* Provides default sources, sinks and sanitizers for reasoning about
* LDAP injection vulnerabilities, as well as extension points for
* adding your own.
*/
import javascript
module LdapInjection {
import Ldapjs::Ldapjs
/**
* A data flow source for LDAP injection vulnerabilities.
*/
abstract class Source extends DataFlow::Node { }
/**
* A data flow sink for LDAP injection vulnerabilities.
*/
abstract class Sink extends DataFlow::Node {
/**
* Gets the LDAP query call that the sink flows into.
*/
abstract DataFlow::Node getQueryCall();
}
/**
* A sanitizer for LDAP injection vulnerabilities.
*/
abstract class Sanitizer extends DataFlow::Node { }
/**
* A source of remote user input, considered as a flow source for LDAP injection.
*/
class RemoteSource extends Source {
RemoteSource() { this instanceof RemoteFlowSource }
}
/**
* An LDAP filter for an API call that executes an operation against the LDAP server.
*/
class LdapjsSearchFilterAsSink extends Sink {
LdapjsSearchFilterAsSink() { this instanceof LdapjsSearchFilter }
override DataFlow::InvokeNode getQueryCall() {
result = this.(LdapjsSearchFilter).getQueryCall()
}
}
/**
* An LDAP DN argument for an API call that executes an operation against the LDAP server.
*/
class LdapjsDNArgumentAsSink extends Sink {
LdapjsDNArgumentAsSink() { this instanceof LdapjsDNArgument }
override DataFlow::InvokeNode getQueryCall() { result = this.(LdapjsDNArgument).getQueryCall() }
}
/**
* A call to a function whose name suggests that it escapes LDAP search query parameter.
*/
class FilterOrDNSanitizationCall extends Sanitizer, DataFlow::CallNode {
FilterOrDNSanitizationCall() {
exists(string sanitize, string input |
sanitize = "(?:escape|saniti[sz]e|validate|filter)" and
input = "[Ii]nput?"
|
this.getCalleeName()
.regexpMatch("(?i)(" + sanitize + input + ")" + "|(" + input + sanitize + ")")
)
}
}
}

View File

@@ -1,92 +0,0 @@
/**
* Provides classes for working with [ldapjs](https://github.com/ldapjs/node-ldapjs) (Client only)
*/
import javascript
module Ldapjs {
/**
* Gets a method name on an LDAPjs client that accepts a DN as the first argument.
*/
private string getLdapjsClientDNMethodName() {
result = ["add", "bind", "compare", "del", "modify", "modifyDN", "search"]
}
/**
* Gets a data flow source node for an LDAP client.
*/
abstract class LdapClient extends DataFlow::SourceNode { }
/**
* Gets a data flow source node for the ldapjs library.
*/
private DataFlow::SourceNode ldapjs() { result = DataFlow::moduleImport("ldapjs") }
/**
* Gets a data flow source node for the ldapjs client.
*/
class LdapjsClient extends LdapClient {
LdapjsClient() { this = ldapjs().getAMemberCall("createClient") }
}
/**
* Gets a data flow node for the client `search` options.
*/
class LdapjsSearchOptions extends DataFlow::SourceNode {
DataFlow::CallNode queryCall;
LdapjsSearchOptions() {
queryCall = any(LdapjsClient client).getAMemberCall("search") and
this = queryCall.getArgument(1).getALocalSource()
}
/**
* Gets the LDAP query call that these options are used in.
*/
DataFlow::InvokeNode getQueryCall() { result = queryCall }
}
/**
* A filter used in a `search` operation against the LDAP server.
*/
class LdapjsSearchFilter extends DataFlow::Node {
LdapjsSearchOptions options;
LdapjsSearchFilter() { this = options.getAPropertyWrite("filter").getRhs() }
/**
* Gets the LDAP query call that this filter is used in.
*/
DataFlow::InvokeNode getQueryCall() { result = options.getQueryCall() }
}
/**
* A call to the ldapjs Client API methods.
*/
class LdapjsClientAPICall extends DataFlow::CallNode {
LdapjsClientAPICall() {
this = any(LdapjsClient client).getAMemberCall(getLdapjsClientDNMethodName())
}
}
/**
* A distinguished name (DN) used in a Client API call against the LDAP server.
*/
class LdapjsDNArgument extends DataFlow::Node {
LdapjsClientAPICall queryCall;
LdapjsDNArgument() { this = queryCall.getArgument(0) }
/**
* Gets the LDAP query call that this DN is used in.
*/
DataFlow::InvokeNode getQueryCall() { result = queryCall }
}
/**
* Ldapjs parseFilter method call.
*/
class LdapjsParseFilter extends DataFlow::CallNode {
LdapjsParseFilter() { this = ldapjs().getAMemberCall("parseFilter") }
}
}

View File

@@ -1,21 +0,0 @@
const http = require('http');
const url = require('url');
const ldap = require('ldapjs');
const client = ldap.createClient({
url: 'ldap://127.0.0.1:1389'
});
const server = http.createServer((req, res) => {
let q = url.parse(req.url, true);
let username = q.query.username;
var opts = {
// BAD
filter: `(|(name=${username})(username=${username}))`
};
client.search('o=example', opts, function (err, res) {
});
});

View File

@@ -1,16 +0,0 @@
const http = require('http');
const url = require('url');
const ldap = require('ldapjs');
const client = ldap.createClient({
url: 'ldap://127.0.0.1:1389'
});
const server = http.createServer((req, res) => {
let q = url.parse(req.url, true);
let username = q.query.username;
// BAD
client.search('o=example', { filter: `(|(name=${username})(username=${username}))` }, function (err, res) {
});
});

View File

@@ -1,31 +0,0 @@
const http = require('http');
const url = require('url');
const ldap = require('ldapjs');
const client = ldap.createClient({
url: 'ldap://127.0.0.1:1389'
});
// https://github.com/vesse/node-ldapauth-fork/commit/3feea43e243698bcaeffa904a7324f4d96df60e4
const sanitizeInput = function (input) {
return input
.replace(/\*/g, '\\2a')
.replace(/\(/g, '\\28')
.replace(/\)/g, '\\29')
.replace(/\\/g, '\\5c')
.replace(/\0/g, '\\00')
.replace(/\//g, '\\2f');
};
const server = http.createServer((req, res) => {
let q = url.parse(req.url, true);
let username = q.query.username;
// GOOD
username = sanitizeInput(username);
client.search('o=example', { filter: `(|(name=${username})(username=${username}))` }, function (err, res) {
});
});

View File

@@ -1,29 +0,0 @@
const http = require('http');
const url = require('url');
const ldap = require('ldapjs');
const client = ldap.createClient({
url: 'ldap://127.0.0.1:1389'
});
const server = http.createServer((req, res) => {
let q = url.parse(req.url, true);
let username = q.query.username;
// GOOD (https://github.com/ldapjs/node-ldapjs/issues/181)
let f = new OrFilter({
filters: [
new EqualityFilter({
attribute: 'name',
value: username
}),
new EqualityFilter({
attribute: 'username',
value: username
})
]
});
client.search('o=example', { filter: f }, function (err, res) {
});
});

View File

@@ -1,25 +0,0 @@
<!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 those 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

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

View File

@@ -1,26 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Failing to set the 'secure' flag on a cookie can cause it to be sent in cleartext.
This makes it easier for an attacker to intercept.</p>
</overview>
<recommendation>
<p>Always set the <code>secure</code> flag to `true` on a cookie before adding it
to an HTTP response (if the default value is `false`).</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#cookiesecure">cookie.secure</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>
<li><a href="https://github.com/js-cookie/js-cookie">js-cookie</a>.</li>
</references>
</qhelp>

View File

@@ -1,18 +0,0 @@
/**
* @name Failure to set secure cookies
* @description Insecure cookies may be sent in cleartext, which makes them vulnerable to
* interception.
* @kind problem
* @problem.severity error
* @precision high
* @id js/insecure-cookie
* @tags security
* external/cwe/cwe-614
*/
import javascript
import experimental.semmle.javascript.security.InsecureCookie::Cookie
from Cookie cookie
where not cookie.isSecure()
select cookie, "Cookie is added to response without the 'secure' flag being set to true"

View File

@@ -0,0 +1,15 @@
const axios = require('axios');
export const handler = async (req, res, next) => {
const { target } = req.body;
try {
// BAD: `target` is controlled by the attacker
const response = await axios.get('https://example.com/current_api/' + target);
// process request response
use(response);
} catch (err) {
// process error
}
};

View File

@@ -0,0 +1,49 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Directly incorporating user input into an HTTP request without validating the input can facilitate
server side request forgery attacks, where the attacker essentially controls the request.
</p>
</overview>
<recommendation>
<p>
To guard against server side request forgery, it is advisable to avoid putting user input directly into a
network request. If using user input is necessary, then is mandatory to validate them. Only allow numeric and alphanumeric values.
URL encoding is not a solution in certain scenarios, such as, an architecture build over NGINX proxies.
</p>
</recommendation>
<example>
<p>
The following example shows an HTTP request parameter being used directly in a URL request without
validating the input, which facilitates an SSRF attack. The request <code>axios.get("https://example.com/current_api/"+target)</code> is
vulnerable since attackers can choose the value of <code>target</code> to be anything they want. For
instance, the attacker can choose <code>"../super_secret_api"</code> as the target, causing the
URL to become <code>"https://example.com/super_secret_api"</code>.
</p>
<p>
A request to <code>https://example.com/super_secret_api</code> may be problematic if that api is not
meant to be directly accessible from the attacker's machine.
</p>
<sample src="SSRF.js"/>
<p>
One way to remedy the problem is to validate the user input to only allow alphanumeric values:
</p>
<sample src="SSRFGood.js"/>
</example>
<references>
<li>OWASP: <a href="https://www.owasp.org/www-community/attacks/Server_Side_Request_Forgery">SSRF</a></li>
</references>
</qhelp>

View File

@@ -0,0 +1,19 @@
/**
* @id javascript/ssrf
* @kind path-problem
* @name Uncontrolled data used in network request
* @description Sending network requests with user-controlled data as part of the URL allows for request forgery attacks.
* @problem.severity error
* @precision medium
* @tags security
* external/cwe/cwe-918
*/
import javascript
import SSRF
import DataFlow::PathGraph
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, DataFlow::Node request
where
cfg.hasFlowPath(source, sink) and request = sink.getNode().(RequestForgery::Sink).getARequest()
select sink, source, sink, "The URL of this request depends on a user-provided value"

View File

@@ -0,0 +1,154 @@
import javascript
import semmle.javascript.security.dataflow.RequestForgeryCustomizations
import semmle.javascript.security.dataflow.UrlConcatenation
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "SSRF" }
override predicate isSource(DataFlow::Node source) { source instanceof RequestForgery::Source }
override predicate isSink(DataFlow::Node sink) { sink instanceof RequestForgery::Sink }
override predicate isSanitizer(DataFlow::Node node) {
super.isSanitizer(node) or
node instanceof RequestForgery::Sanitizer
}
private predicate hasSanitizingSubstring(DataFlow::Node nd) {
nd.getStringValue().regexpMatch(".*[?#].*")
or
hasSanitizingSubstring(StringConcatenation::getAnOperand(nd))
or
hasSanitizingSubstring(nd.getAPredecessor())
}
private predicate strictSanitizingPrefixEdge(DataFlow::Node source, DataFlow::Node sink) {
exists(DataFlow::Node operator, int n |
StringConcatenation::taintStep(source, sink, operator, n) and
hasSanitizingSubstring(StringConcatenation::getOperand(operator, [0 .. n - 1]))
)
}
override predicate isSanitizerEdge(DataFlow::Node source, DataFlow::Node sink) {
strictSanitizingPrefixEdge(source, sink)
}
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode nd) {
nd instanceof IntegerCheck or
nd instanceof ValidatorCheck or
nd instanceof TernaryOperatorSanitizerGuard
}
}
/**
* This sanitizers models the next example:
* let valid = req.params.id ? Number.isInteger(req.params.id) : false
* if (valid) { sink(req.params.id) }
*
* This sanitizer models this way of using ternary operators,
* when the sanitizer guard is used as any of the branches
* instead of being used as the condition.
*
* This sanitizer sanitize the corresponding if statement branch.
*/
class TernaryOperatorSanitizer extends RequestForgery::Sanitizer {
TernaryOperatorSanitizer() {
exists(
TaintTracking::SanitizerGuardNode guard, IfStmt ifStmt, DataFlow::Node taintedInput,
boolean outcome, Stmt r, DataFlow::Node falseNode
|
ifStmt.getCondition().flow().getAPredecessor+() = guard and
ifStmt.getCondition().flow().getAPredecessor+() = falseNode and
falseNode.asExpr().(BooleanLiteral).mayHaveBooleanValue(false) and
not ifStmt.getCondition() instanceof LogicalBinaryExpr and
guard.sanitizes(outcome, taintedInput.asExpr()) and
(
outcome = true and r = ifStmt.getThen() and not ifStmt.getCondition() instanceof LogNotExpr
or
outcome = false and r = ifStmt.getElse() and not ifStmt.getCondition() instanceof LogNotExpr
or
outcome = false and r = ifStmt.getThen() and ifStmt.getCondition() instanceof LogNotExpr
or
outcome = true and r = ifStmt.getElse() and ifStmt.getCondition() instanceof LogNotExpr
) and
r.getFirstControlFlowNode()
.getBasicBlock()
.(ReachableBasicBlock)
.dominates(this.getBasicBlock())
)
}
}
/**
* This sanitizer guard is another way of modeling the example from above
* In this case:
* let valid = req.params.id ? Number.isInteger(req.params.id) : false
* if (!valid) { return }
* sink(req.params.id)
*
* The previous sanitizer is not enough,
* because we are sanitizing the entire if statement branch
* but we need to sanitize the use of this variable from now on.
*
* Thats why we model this sanitizer guard which says that
* the result of the ternary operator execution is a sanitizer guard.
*/
class TernaryOperatorSanitizerGuard extends TaintTracking::SanitizerGuardNode {
TaintTracking::SanitizerGuardNode originalGuard;
TernaryOperatorSanitizerGuard() {
this.getAPredecessor+().asExpr().(BooleanLiteral).mayHaveBooleanValue(false) and
this.getAPredecessor+() = originalGuard and
not this.asExpr() instanceof LogicalBinaryExpr
}
override predicate sanitizes(boolean outcome, Expr e) {
not this.asExpr() instanceof LogNotExpr and
originalGuard.sanitizes(outcome, e)
or
exists(boolean originalOutcome |
this.asExpr() instanceof LogNotExpr and
originalGuard.sanitizes(originalOutcome, e) and
(
originalOutcome = true and outcome = false
or
originalOutcome = false and outcome = true
)
)
}
}
/**
* Number.isInteger is a sanitizer guard because a number can't be used to exploit a SSRF.
*/
class IntegerCheck extends TaintTracking::SanitizerGuardNode, DataFlow::CallNode {
IntegerCheck() { this = DataFlow::globalVarRef("Number").getAMemberCall("isInteger") }
override predicate sanitizes(boolean outcome, Expr e) {
outcome = true and
e = getArgument(0).asExpr()
}
}
/**
* ValidatorCheck identifies if exists a call to validator's library methods.
* validator is a library which has a variety of input-validation functions. We are interesed in
* checking that source is a number (any type of number) or an alphanumeric value.
*/
class ValidatorCheck extends TaintTracking::SanitizerGuardNode, DataFlow::CallNode {
ValidatorCheck() {
exists(DataFlow::SourceNode mod, string method |
mod = DataFlow::moduleImport("validator") and
this = mod.getAChainedMethodCall(method) and
method in [
"isAlphanumeric", "isAlpha", "isDecimal", "isFloat", "isHexadecimal", "isHexColor",
"isInt", "isNumeric", "isOctal", "isUUID"
]
)
}
override predicate sanitizes(boolean outcome, Expr e) {
outcome = true and
e = getArgument(0).asExpr()
}
}

View File

@@ -0,0 +1,20 @@
const axios = require('axios');
const validator = require('validator');
export const handler = async (req, res, next) => {
const { target } = req.body;
if (!validator.isAlphanumeric(target)) {
return next(new Error('Bad request'));
}
try {
// `target` is validated
const response = await axios.get('https://example.com/current_api/' + target);
// process request response
use(response);
} catch (err) {
// process error
}
};

View File

@@ -1,323 +0,0 @@
/**
* Provides classes for reasoning about cookies added to response without the 'secure' or 'httponly' 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 maliciously injected JavaScript.
*/
import javascript
module Cookie {
/**
* `secure` property of the cookie options.
*/
string secureFlag() { result = "secure" }
/**
* `httpOnly` property of the cookie options.
*/
string httpOnlyFlag() { result = "httpOnly" }
/**
* Abstract class to represent different cases of insecure cookie settings.
*/
abstract class Cookie extends DataFlow::Node {
/**
* Gets the name of the middleware/library used to set the cookie.
*/
abstract string getKind();
/**
* Gets the options used to set this cookie, if any.
*/
abstract DataFlow::Node getCookieOptionsArgument();
/**
* 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.
*/
private predicate isAuthVariable(DataFlow::Node expr) {
exists(string val |
(
val = expr.getStringValue() or
val = expr.asExpr().(VarAccess).getName() or
val = expr.(DataFlow::PropRead).getPropertyName()
) and
regexpMatchAuth(val)
)
or
isAuthVariable(expr.getAPredecessor())
}
/**
* Holds if `val` looks related to authentication, without being an anti-forgery token.
*/
bindingset[val]
private predicate regexpMatchAuth(string val) {
val.regexpMatch("(?i).*(session|login|token|user|auth|credential).*") and
not val.regexpMatch("(?i).*(xsrf|csrf|forgery).*")
}
/**
* A cookie set using the `express` module `cookie-session` (https://github.com/expressjs/cookie-session).
*/
class InsecureCookieSession extends ExpressLibraries::CookieSession::MiddlewareInstance, Cookie {
override string getKind() { result = "cookie-session" }
override DataFlow::SourceNode getCookieOptionsArgument() { result.flowsTo(getArgument(0)) }
private DataFlow::Node getCookieFlagValue(string flag) {
result = this.getCookieOptionsArgument().getAPropertyWrite(flag).getRhs()
}
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(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)
}
}
/**
* A cookie set using the `express` module `express-session` (https://github.com/expressjs/session).
*/
class InsecureExpressSessionCookie extends ExpressLibraries::ExpressSession::MiddlewareInstance,
Cookie {
override string getKind() { result = "express-session" }
override DataFlow::SourceNode getCookieOptionsArgument() { result = this.getOption("cookie") }
private DataFlow::Node getCookieFlagValue(string flag) {
result = this.getCookieOptionsArgument().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 }.
// A cookie is secure if there are the cookie options with the `secure` flag set to `true` or to `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#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 getCookieFlagValue(httpOnlyFlag()).mayHaveBooleanValue(false)
}
}
/**
* A cookie set using `response.cookie` from `express` module (https://expressjs.com/en/api.html#res.cookie).
*/
class InsecureExpressCookieResponse extends Cookie, DataFlow::MethodCallNode {
InsecureExpressCookieResponse() { this.calls(any(Express::ResponseExpr r).flow(), "cookie") }
override string getKind() { result = "response.cookie" }
override DataFlow::SourceNode getCookieOptionsArgument() {
result = this.getLastArgument().getALocalSource()
}
private DataFlow::Node getCookieFlagValue(string flag) {
result = this.getCookieOptionsArgument().getAPropertyWrite(flag).getRhs()
}
override predicate isSecure() {
// A cookie is secure if there are cookie options with the `secure` flag set to `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)
}
}
private class AttributeToSetCookieHeaderTrackingConfig extends TaintTracking::Configuration {
AttributeToSetCookieHeaderTrackingConfig() { this = "AttributeToSetCookieHeaderTrackingConfig" }
override predicate isSource(DataFlow::Node source) {
exists(string s | source.mayHaveStringValue(s))
}
override predicate isSink(DataFlow::Node sink) { sink.asExpr() instanceof TemplateLiteral }
}
private class SensitiveNameToSetCookieHeaderTrackingConfig extends TaintTracking::Configuration {
SensitiveNameToSetCookieHeaderTrackingConfig() {
this = "SensitiveNameToSetCookieHeaderTrackingConfig"
}
override predicate isSource(DataFlow::Node source) { isAuthVariable(source) }
override predicate isSink(DataFlow::Node sink) { sink.asExpr() instanceof TemplateLiteral }
}
/**
* A cookie set using `Set-Cookie` header of an `HTTP` response.
* (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).
* In case an array is passed `setHeader("Set-Cookie", [...]` it sets multiple cookies.
* Each array element has its own attributes.
*/
class InsecureSetCookieHeader extends Cookie {
InsecureSetCookieHeader() {
this.asExpr() = any(HTTP::SetCookieHeader setCookie).getHeaderArgument()
}
override string getKind() { result = "set-cookie header" }
override DataFlow::Node getCookieOptionsArgument() {
if this.asExpr() instanceof ArrayExpr
then result.asExpr() = this.asExpr().(ArrayExpr).getAnElement()
else result.asExpr() = this.asExpr()
}
/**
* A cookie is secure if the `secure` flag is specified in the cookie definition.
* The default is `false`.
*/
override predicate isSecure() { allHaveCookieAttribute("secure") }
/**
* A cookie is httpOnly if the `httpOnly` flag is specified in the cookie definition.
* The default is `false`.
*/
override predicate isHttpOnly() { allHaveCookieAttribute(httpOnlyFlag()) }
/**
* The predicate holds only if all elements have the specified attribute.
*/
bindingset[attribute]
private predicate allHaveCookieAttribute(string attribute) {
forall(DataFlow::Node n | n = getCookieOptionsArgument() |
exists(string s |
n.mayHaveStringValue(s) and
hasCookieAttribute(s, attribute)
)
or
exists(AttributeToSetCookieHeaderTrackingConfig cfg, DataFlow::Node source |
cfg.hasFlow(source, n) and
exists(string attr |
source.mayHaveStringValue(attr) and
attr.regexpMatch("(?i).*\\b" + attribute + "\\b.*")
)
)
)
}
/**
* The predicate holds only if any element has a sensitive name and
* doesn't have the `httpOnly` flag.
*/
override predicate isAuthNotHttpOnly() {
exists(DataFlow::Node n | n = getCookieOptionsArgument() |
exists(string s |
n.mayHaveStringValue(s) and
(
not hasCookieAttribute(s, httpOnlyFlag()) and
regexpMatchAuth(getCookieName(s))
)
)
or
not exists(AttributeToSetCookieHeaderTrackingConfig cfg, DataFlow::Node source |
cfg.hasFlow(source, n) and
exists(string attr |
source.mayHaveStringValue(attr) and
attr.regexpMatch("(?i).*\\b" + httpOnlyFlag() + "\\b.*")
)
) and
exists(SensitiveNameToSetCookieHeaderTrackingConfig cfg | cfg.hasFlow(_, n))
)
}
/**
* Gets cookie name from 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("\\s*([^=\\s]*)\\s*=.*", 1) }
/**
* 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 + "\\s*;?.*$")
}
}
/**
* A cookie set using `js-cookie` library (https://github.com/js-cookie/js-cookie).
*/
class InsecureJsCookie extends Cookie {
InsecureJsCookie() {
this =
[
DataFlow::globalVarRef("Cookie"),
DataFlow::globalVarRef("Cookie").getAMemberCall("noConflict"),
DataFlow::moduleImport("js-cookie")
].getAMemberCall("set")
}
override string getKind() { result = "js-cookie" }
override DataFlow::SourceNode getCookieOptionsArgument() {
result = this.(DataFlow::CallNode).getAnArgument().getALocalSource()
}
DataFlow::Node getCookieFlagValue(string flag) {
result = this.getCookieOptionsArgument().getAPropertyWrite(flag).getRhs()
}
override predicate isSecure() {
// A cookie is secure if there are cookie options with the `secure` flag set to `true`.
getCookieFlagValue(secureFlag()).mayHaveBooleanValue(true)
}
override predicate isAuthNotHttpOnly() { none() }
override predicate isHttpOnly() { none() } // js-cookie is browser side library and doesn't support HttpOnly
}
}