8000 JS: Add experimental variants of common security queries with more sources by erik-krogh · Pull Request #11582 · github/codeql · GitHub
[go: up one dir, main page]

Skip to content

JS: Add experimental variants of common security queries with more sources #11582

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 4, 2023

Conversation

erik-krogh
Copy link
Contributor
@erik-krogh erik-krogh commented Dec 6, 2022

The approach I took was to implement a semantic patch (see backref) that converts the queries into heuristic variants.
And some scripting that converts the existing .qhelp files to .inc.qhelp files and adds .qhelp files with <include ../> in the right locations.
Nothing in the JS: add heuristic variants of queries that use RemoteFlowSource commit was written manually.

I did an evaluation (on this commit) to confirm that the new queries dont blow up.

The QL-for-QL alert is a TP (and a nice find).
That CI check will stay red, but we can merge anyway.
We will get re-evaluation of the cached RemoteFlowSource layer when evaluating these new experimental queries.
I find this acceptable, given that these queries need to be explicitly enabled, and there should only be one re-evaluation.

The QL-for-QL change is needed for the patch, so I've included it in this PR.

This PR nicely complements the new query-suite added by turbo: #11702

8000

@erik-krogh erik-krogh added WIP This is a work-in-progress, do not merge yet! Awaiting evaluation Do not merge yet, this PR is waiting for an evaluation to finish labels Dec 6, 2022
@github-actions github-actions bot added the JS label Dec 6, 2022
@github-actions
Copy link
Contributor
github-actions bot commented Dec 7, 2022

QHelp previews:

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-020/UntrustedDataToExternalAPI.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-020/UntrustedDataToExternalAPI.qhelp: Could not find sample ExternalAPISinkExample.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-020/UntrustedDataToExternalAPI.qhelp: Could not find sample ExternalAPITaintStepExample.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-078/CommandInjection.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-078/CommandInjection.qhelp: Could not find sample examples/command-injection.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-079/Xss.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-079/Xss.qhelp: Could not find sample examples/Xss.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-089/SqlInjection.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-089/SqlInjection.qhelp: Could not find sample examples/SqlInjection.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-094/CodeInjection.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-094/CodeInjection.qhelp: Could not find sample examples/CodeInjection.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-094/CodeInjection.qhelp: Could not find sample examples/ServerSideTemplateInjection.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-094/CodeInjection.qhelp: Could not find sample examples/ServerSideTemplateInjectionSafe.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-117/LogInjection.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-117/LogInjection.qhelp: Could not find sample examples/logInjectionBad.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-117/LogInjection.qhelp: Could not find sample examples/logInjectionGood.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-134/TaintedFormatString.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-134/TaintedFormatString.qhelp: Could not find sample examples/TaintedFormatStringBad.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-134/TaintedFormatString.qhelp: Could not find sample examples/TaintedFormatStringGood.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-346/CorsMisconfigurationForCredentials.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-346/CorsMisconfigurationForCredentials.qhelp: Could not find sample examples/CorsMisconfigurationForCredentials.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-346/CorsMisconfigurationForCredentials.qhelp: Could not find sample examples/CorsMisconfigurationForCredentials_fixed.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-400/RemotePropertyInjection.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-400/RemotePropertyInjection.qhelp: Could not find sample examples/RemotePropertyInjection.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-400/RemotePropertyInjection.qhelp: Could not find sample examples/RemotePropertyInjection_fixed.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-502/UnsafeDeserialization.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-502/UnsafeDeserialization.qhelp: Could not find sample examples/UnsafeDeserializationBad.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-502/UnsafeDeserialization.qhelp: Could not find sample examples/UnsafeDeserializationGood.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-611/Xxe.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-611/Xxe.qhelp: Could not find sample examples/Xxe.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-611/Xxe.qhelp: Could not find sample examples/XxeGood.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-643/XpathInjection.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-643/XpathInjection.qhelp: Could not find sample examples/XpathInjectionBad.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-643/XpathInjection.qhelp: Could not find sample examples/XpathInjectionGood.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-730/RegExpInjection.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-730/RegExpInjection.qhelp: Could not find sample examples/RegExpInjection.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-730/RegExpInjection.qhelp: Could not find sample examples/RegExpInjectionGood.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-770/ResourceExhaustion.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-770/ResourceExhaustion.qhelp: Could not find sample examples/ResourceExhaustion_buffer.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-770/ResourceExhaustion.qhelp: Could not find sample examples/ResourceExhaustion_buffer_fixed.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-770/ResourceExhaustion.qhelp: Could not find sample examples/ResourceExhaustion_array.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-770/ResourceExhaustion.qhelp: Could not find sample examples/ResourceExhaustion_array_fixed.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-770/ResourceExhaustion.qhelp: Could not find sample examples/ResourceExhaustion_timeout.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-770/ResourceExhaustion.qhelp: Could not find sample examples/ResourceExhaustion_timeout_fixed.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-776/XmlBomb.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-776/XmlBomb.qhelp: Could not find sample examples/XmlBomb.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-776/XmlBomb.qhelp: Could not find sample examples/XmlBombGood.js
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-807/ConditionalBypass.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-807/ConditionalBypass.qhelp: Could not find include file recommendation.inc.qhelp
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-807/ConditionalBypass.qhelp: Could not find include file example.inc.qhelp
A fatal error occurred: 1 qhelp files could not be processed.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-915/PrototypePollutingAssignment.qhelp

errors/warnings:

./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-915/PrototypePollutingAssignment.qhelp: Could not find sample examples/PrototypePollutingAssignment.js
./javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-915/PrototypePollutingAssignment.qhelp: Could not find sample examples/PrototypePollutingAssignmentFixed.js
A fatal error occurred: 1 qhelp files could not be processed.

@github-actions
Copy link
Contributor
github-actions bot commented Dec 14, 2022

QHelp previews:

javascript/ql/src/Security/CWE-020/UntrustedDataToExternalAPI.qhelp

Untrusted data passed to external API

Using unsanitized untrusted data in an external API can cause a variety of security issues. This query reports external APIs that use untrusted data. The results are not filtered so that you can audit all examples. The query provides data for security reviews of the application and you can also use it to identify external APIs that should be modeled as either taint steps, or sinks for specific problems.

An external API is defined as a method call to a method that is not defined in the source code, not overridden in the source code, and is not modeled as a taint step in the default taint library. External APIs may be from the third-party dependencies or from internal dependencies. The query reports uses of untrusted data one of the arguments of external API call or in the return value from a callback passed to an external API.

Recommendation

For each result:

  • If the result highlights a known sink, confirm that the result is reported by the relevant query, or that the result is a false positive because this data is sanitized.
  • If the result highlights an unknown sink for a problem, then add modeling for the sink to the relevant query, and confirm that the result is either found, or is safe due to appropriate sanitization.
  • If the result represents a call to an external API that transfers taint, add the appropriate modeling, and re-run the query to determine what new results have appeared due to this additional modeling.
    Otherwise, the result is likely uninteresting. Custom versions of this query can extend the SafeExternalAPIMethod class to exclude known safe external APIs from future analysis.

Example

In this first example, a query parameter is read from the req parameter and then ultimately used in a call to the res.send external API:

express().get('/news', (req, res) => {
    let topic = req.query.topic;
    res.send(`<h1>${topic}</h1>`);
});

This is a reflected XSS sink. The XSS query should therefore be reviewed to confirm that this sink is appropriately modeled, and if it is, to confirm that the query reports this particular result, or that the result is a false positive due to some existing sanitization.

In this second example, again a query parameter is read from req.

let path = require('path');

express().get('/data', (req, res) => {
    let file = path.join(HOME_DIR, 'public', req.query.file);
    res.sendFile(file);
});

If the query reported the call to path.join on line 4, this would suggest that this external API is not currently modeled as a taint step in the taint tracking library. The next step would be to model this as a taint step, then re-run the query to determine what additional results might be found. In this example, it seems the result of the path.join will be used as a file path, leading to a path traversal vulnerability.

Note that both examples are correctly handled by the standard taint tracking library and security queries.

References

  • Common Weakness Enumeration: CWE-20.
javascript/ql/src/Security/CWE-078/CommandInjection.qhelp

Uncontrolled command line

Code that passes user input directly to require('child_process').exec, or some other library routine that executes a command, allows the user to execute malicious code.

Recommendation

If possible, use hard-coded string literals to specify the command to run or library to load. Instead of passing the user input directly to the process or library function, examine the user input and then choose among hard-coded string literals.

If the applicable libraries or commands cannot be determined at compile time, then add code to verify that the user input string is safe before using it.

Example

The following example shows code that takes a shell script that can be changed maliciously by a user, and passes it straight to child_process.exec without examining it first.

var cp = require("child_process"),
    http = require('http'),
    url = require('url');

var server = http.createServer(function(req, res) {
    let cmd = url.parse(req.url, true).query.path;

    cp.exec(cmd); // BAD
});

References

javascript/ql/src/Security/CWE-079/Xss.qhelp

Client-side cross-site scripting

Directly writing user input (for example, a URL query parameter) to a webpage without properly sanitizing the input first, allows for a cross-site scripting vulnerability.

This kind of vulnerability is also called DOM-based cross-site scripting, to distinguish it from other types of cross-site scripting.

Recommendation

To guard against cross-site scripting, consider using contextual output encoding/escaping before writing user input to the page, or one of the other solutions that are mentioned in the references.

Example

The following example shows part of the page URL being written directly to the document, leaving the website vulnerable to cross-site scripting.

function setLanguageOptions() {
    var href = document.location.href,
        deflt = href.substring(href.indexOf("default=")+8);
    document.write("<OPTION value=1>"+deflt+"</OPTION>");
    document.write("<OPTION value=2>English</OPTION>");
}

References

javascript/ql/src/Security/CWE-089/SqlInjection.qhelp

Database query built from user-controlled sources

If a database query (such as a SQL or NoSQL query) is built from user-provided data without sufficient sanitization, a malicious user may be able to run malicious database queries.

Recommendation

Most database connector libraries offer a way of safely embedding untrusted data into a query by means of query parameters or prepared statements.

For NoSQL queries, make use of an operator like MongoDB's $eq to ensure that untrusted data is interpreted as a literal value and not as a query object.

Example

In the following example, assume the function handler is an HTTP request handler in a web application, whose parameter req contains the request object.

The handler constructs two copies of the same SQL query involving user input taken from the request object, once unsafely using string concatenation, and once safely using query parameters.

In the first case, the query string query1 is built by directly concatenating a user-supplied request parameter with some string literals. The parameter may include quote characters, so this code is vulnerable to a SQL injection attack.

In the second case, the parameter is embedded into the query string query2 using query parameters. In this example, we use the API offered by the pg Postgres database connector library, but other libraries offer similar features. This version is immune to injection attacks.

const app = require("express")(),
      pg = require("pg"),
      pool = new pg.Pool(config);

app.get("search", function handler(req, res) {
  // BAD: the category might have SQL special characters in it
  var query1 =
    "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='" +
    req.params.category +
    "' ORDER BY PRICE";
  pool.query(query1, [], function(err, results) {
    // process results
  });

  // GOOD: use parameters
  var query2 =
    "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY=$1" + " ORDER BY PRICE";
  pool.query(query2, [req.params.category], function(err, results) {
    // process results
  });
});

References

javascript/ql/src/Security/CWE-094/CodeInjection.qhelp

Code injection

Directly evaluating user input (for example, an HTTP request parameter) as code without properly sanitizing the input first allows an attacker arbitrary code execution. This can occur when user input is treated as JavaScript, or passed to a framework which interprets it as an expression to be evaluated. Examples include AngularJS expressions or JQuery selectors.

Recommendation

Avoid including user input in any expression which may be dynamically evaluated. If user input must be included, use context-specific escaping before including it. It is important that the correct escaping is used for the type of evaluation that will occur.

Example

The following example shows part of the page URL being evaluated as JavaScript code. This allows an attacker to provide JavaScript within the URL. If an attacker can persuade a user to click on a link to such a URL, the attacker can evaluate arbitrary JavaScript in the browser of the user to, for example, steal cookies containing session information.

eval(document.location.href.substring(document.location.href.indexOf("default=")+8))

The following example shows a Pug template being constructed from user input, allowing attackers to run arbitrary code via a payload such as #{global.process.exit(1)}.

const express = require('express')
var pug = require('pug');
const app = express()

app.post('/', (req, res) => {
    var input = req.query.username;
    var template = `
doctype
html
head
    title= 'Hello world'
body
    form(action='/' method='post')
        input#name.form-control(type='text)
        button.btn.btn-primary(type='submit') Submit
    p Hello `+ input
    var fn = pug.compile(template);
    var html = fn();
    res.send(html);
})

Below is an example of how to use a template engine without any risk of template injection. The user input is included via an interpolation expression #{username} whose value is provided as an option to the template, instead of being part of the template string itself:

const express = require('express')
var pug = require('pug');
const app = express()

app.post('/', (req, res) => {
    var input = req.query.username;
    var template = `
doctype
html
head
    title= 'Hello world'
body
    form(action='/' method='post')
        input#name.form-control(type='text)
        button.btn.btn-primary(type='submit') Submit
    p Hello #{username}`
    var fn = pug.compile(template);
    var html = fn({username: input});
    res.send(html);
})

References

javascript/ql/src/Security/CWE-117/LogInjection.qhelp

Log injection

If unsanitized user input is written to a log entry, a malicious user may be able to forge new log entries.

Forgery can occur if a user provides some input with characters that are interpreted when the log output is displayed. If the log is displayed as a plain text file, then new line characters can be used by a malicious user. If the log is displayed as HTML, then arbitrary HTML may be included to spoof log entries.

Recommendation

User input should be suitably sanitized before it is logged.

If the log entries are in plain text then line breaks should be removed from user input, using String.prototype.replace or similar. Care should also be taken that user input is clearly marked in log entries.

For log entries that will be displayed in HTML, user input should be HTML-encoded before being logged, to prevent forgery and other forms of HTML injection.

Example

In the first example, a username, provided by the user, is logged using `console.info`. In the first case, it is logged without any sanitization. In the second case, the username is used to build an error that is logged using `console.error`. If a malicious user provides `username=Guest%0a[INFO]+User:+Admin%0a` as a username parameter, the log entry will be splitted in two different lines, where the second line will be `[INFO]+User:+Admin`.

const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
    let q = url.parse(req.url, true);

    console.info(`[INFO] User: ${q.query.username}`); // BAD: User input logged as-is
})

server.listen(3000, '127.0.0.1', () => {});

In the second example, String.prototype.replace is used to ensure no line endings are present in the user input.

const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
    let q = url.parse(req.url, true);

    // GOOD: remove newlines from user controlled input before logging
    let username = q.query.username.replace(/\n|\r/g, "");

    console.info(`[INFO] User: ${username}`);
});

server.listen(3000, '127.0.0.1', () => {});

References

javascript/ql/src/Security/CWE-134/TaintedFormatString.qhelp

Use of externally-controlled format string

Functions like the Node.js standard library function util.format accept a format string that is used to format the remaining arguments by providing inline format specifiers. If the format string contains unsanitized input from an untrusted source, then that string may contain unexpected format specifiers that cause garbled output.

Recommendation

Either sanitize the input before including it in the format string, or use a %s specifier in the format string, and pass the untrusted data as corresponding argument.

Example

The following program snippet logs information about an unauthorized access attempt. The log message includes the user name, and the user's IP address is passed as an additional argument to console.log to be appended to the message:

const app = require("express")();

app.get("unauthorized", function handler(req, res) {
  let user = req.query.user;
  let ip = req.connection.remoteAddress;
  console.log("Unauthorized access attempt by " + user, ip);
});

However, if a malicious user provides %d as their user name, console.log will instead attempt to format the ip argument as a number. Since IP addresses are not valid numbers, the result of this conversion is NaN. The resulting log message will read "Unauthorized access attempt by NaN", missing all the information that it was trying to log in the first place.

Instead, the user name should be included using the %s specifier:

const app = require("express")();

app.get("unauthorized", function handler(req, res) {
  let user = req.query.user;
  let ip = req.connection.remoteAddress;
  console.log("Unauthorized access attempt by %s", user, ip);
});

References

javascript/ql/src/Security/CWE-346/CorsMisconfigurationForCredentials.qhelp

CORS misconfiguration for credentials transfer

A server can send the "Access-Control-Allow-Credentials" CORS header to control when a browser may send user credentials in Cross-Origin HTTP requests.

When the Access-Control-Allow-Credentials header is "true", the Access-Control-Allow-Origin header must have a value different from "*" in order to make browsers accept the header. Therefore, to allow multiple origins for Cross-Origin requests with credentials, the server must dynamically compute the value of the "Access-Control-Allow-Origin" header. Computing this header value from information in the request to the server can therefore potentially allow an attacker to control the origins that the browser sends credentials to.

Recommendation

When the Access-Control-Allow-Credentials header value is "true", a dynamic computation of the Access-Control-Allow-Origin header must involve sanitization if it relies on user-controlled input.

Since the "null" origin is easy to obtain for an attacker, it is never safe to use "null" as the value of the Access-Control-Allow-Origin header when the Access-Control-Allow-Credentials header value is "true".

Example

In the example below, the server allows the browser to send user credentials in a Cross-Origin request. The request header origins controls the allowed origins for such a Cross-Origin request.

var https = require('https'),
    url = require('url');

var server = https.createServer(function(){});

server.on('request', function(req, <
8000
span class="pl-s1">res) {
    let origin = url.parse(req.url, true).query.origin;
     // BAD: attacker can choose the value of origin
    res.setHeader("Access-Control-Allow-Origin", origin);
    res.setHeader("Access-Control-Allow-Credentials", true);

    // ...
});

This is not secure, since an attacker can choose the value of the origin request header to make the browser send credentials to their own server. The use of a whitelist containing allowed origins for the Cross-Origin request fixes the issue:

var https = require('https'),
    url = require('url');

var server = https.createServer(function(){});

server.on('request', function(req, res) {
    let origin = url.parse(req.url, true).query.origin,
        whitelist = {
            "https://example.com": true,
            "https://subdomain.example.com": true,
            "https://example.com:1337": true
        };

    if (origin in whitelist) {
        // GOOD: the origin is in the whitelist
        res.setHeader("Access-Control-Allow-Origin", origin);
        res.setHeader("Access-Control-Allow-Credentials", true);
    }

    // ...
});

References

javascript/ql/src/Security/CWE-400/RemotePropertyInjection.qhelp

Remote property injection

Dynamically computing object property names from untrusted input may have multiple undesired consequences. For example, if the property access is used as part of a write, an attacker may overwrite vital properties of objects, such as __proto__. This attack is known as prototype pollution attack and may serve as a vehicle for denial-of-service attacks. A similar attack vector, is to replace the toString property of an object with a primitive. Whenever toString is then called on that object, either explicitly or implicitly as part of a type coercion, an exception will be raised.

Moreover, if the name of an HTTP header is user-controlled, an attacker may exploit this to overwrite security-critical headers such as Access-Control-Allow-Origin or Content-Security-Policy.

Recommendation

The most common case in which prototype pollution vulnerabilities arise is when JavaScript objects are used for implementing map data structures. This case should be avoided whenever possible by using the ECMAScript 2015 Map instead. When this is not possible, an alternative fix is to prepend untrusted input with a marker character such as $, before using it in properties accesses. In this way, the attacker does not have access to built-in properties which do not start with the chosen character.

When using user input as part of a header name, a sanitization step should be performed on the input to ensure that the name does not clash with existing header names such as Content-Security-Policy.

Example

In the example below, the dynamically computed property prop is accessed on myObj using a user-controlled value.

var express = require('express');

var app = express();
var myObj = {}

app.get('/user/:id', function(req, res) {
	var prop = req.query.userControlled; // BAD
	myObj[prop] = function() {};
	console.log("Request object " + myObj);
});

This is not secure since an attacker may exploit this code to overwrite the property __proto__ with an empty function. If this happens, the concatenation in the console.log argument will fail with a confusing message such as "Function.prototype.toString is not generic". If the application does not properly handle this error, this scenario may result in a serious denial-of-service attack. The fix is to prepend the user-controlled string with a marker character such as $ which will prevent arbitrary property names from being overwritten.

var express = require('express');

var app = express();
var myObj = {}

app.get('/user/:id', function(req, res) {
	var prop = "$" + req.query.userControlled; // GOOD
	myObj[prop] = function() {};
	console.log("Request object " + myObj);
});

References

javascript/ql/src/Security/CWE-502/UnsafeDeserialization.qhelp

Deserialization of user-controlled data

Deserializing untrusted data using any deserialization framework that allows the construction of arbitrary functions is easily exploitable and, in many cases, allows an attacker to execute arbitrary code.

Recommendation

Avoid deserialization of untrusted data if at all possible. If the architecture permits it, then use formats like JSON or XML that cannot represent functions. When using YAML or other formats that support the serialization and deserialization of functions, ensure that the parser is configured to disable deserialization of arbitrary functions.

Example

The following example calls the load function of the popular js-yaml package on data that comes from an HTTP request and hence is inherently unsafe.

const app = require("express")(),
  jsyaml = require("js-yaml");

app.get("load", function(req, res) {
  let data = jsyaml.load(req.params.data);
  // ...
});

Using the safeLoad function instead (which does not deserialize YAML-encoded functions) removes the vulnerability.

const app = require("express")(),
  jsyaml = require("js-yaml");

app.get("load", function(req, res) {
  let data = jsyaml.safeLoad(req.params.data);
  // ...
});

References

javascript/ql/src/Security/CWE-611/Xxe.qhelp

XML external entity expansion

Parsing untrusted XML files with a weakly configured XML parser may lead to an XML External Entity (XXE) attack. This type of attack uses external entity references to access arbitrary files on a system, carry out denial-of-service (DoS) attacks, or server-side request forgery. Even when the result of parsing is not returned to the user, DoS attacks are still possible and out-of-band data retrieval techniques may allow attackers to steal sensitive data.

Recommendation

The easiest way to prevent XXE attacks is to disable external entity handling when parsing untrusted data. How this is done depends on the library being used. Note that some libraries, such as recent versions of libxml, disable entity expansion by default, so unless you have explicitly enabled entity expansion, no further action needs to be taken.

Example

The following example uses the libxml XML parser to parse a string xmlSrc. If that string is from an untrusted source, this code may be vulnerable to an XXE attack, since the parser is invoked with the noent option set to true:

const app = require("express")(),
  libxml = require("libxmljs");

app.post("upload", (req, res) => {
  let xmlSrc = req.body,
    doc = libxml.parseXml(xmlSrc, { noent: true });
});

To guard against XXE attacks, the noent option should be omitted or set to false. This means that no entity expansion is undertaken at all, not even for standard internal entities such as &amp; or &gt;. If desired, these entities can be expanded in a separate step using utility functions provided by libraries such as underscore, lodash or he.

const app = require("express")(),
  libxml = require("libxmljs");

app.post("upload", (req, res) => {
  let xmlSrc = req.body,
    doc = libxml.parseXml(xmlSrc);
});

References

javascript/ql/src/Security/CWE-643/XpathInjection.qhelp

XPath injection

If an XPath expression is built using string concatenation, and the components of the concatenation include user input, it makes it very easy for a user to create a malicious XPath expression.

Recommendation

If user input must be included in an XPath expression, either sanitize the data or use variable references to safely embed it without altering the structure of the expression.

Example

In this example, the code accepts a user name specified by the user, and uses this unvalidated and unsanitized value in an XPath expression constructed using the xpath package. This is vulnerable to the user providing special characters or string sequences that change the meaning of the XPath expression to search for different values.

const express = require('express');
const xpath = require('xpath');
const app = express();

app.get('/some/route', function(req, res) {
  let userName = req.param("userName");

  // BAD: Use user-provided data directly in an XPath expression
  let badXPathExpr = xpath.parse("//users/user[login/text()='" + userName + "']/home_dir/text()");
  badXPathExpr.select({
    node: root
  });
});

Instead, embed the user input using the variable replacement mechanism offered by xpath:

const express = require('express');
const xpath = require('xpath');
const app = express();

app.get('/some/route', function(req, res) {
  let userName = req.param("userName");

  // GOOD: Embed user-provided data using variables
  let goodXPathExpr = xpath.parse("//users/user[login/text()=$userName]/home_dir/text()");
  goodXPathExpr.select({
    node: root,
    variables: { userName: userName }
  });
});

References

javascript/ql/src/Security/CWE-730/RegExpInjection.qhelp

Regular expression injection

Constructing a regular expression with unsanitized user input is dangerous as a malicious user may be able to modify the meaning of the expression. In particular, such a user may be able to provide a regular expression fragment that takes exponential time in the worst case, and use that to perform a Denial of Service attack.

Recommendation

Before embedding user input into a regular expression, use a sanitization function such as lodash's _.escapeRegExp to escape meta-characters that have special meaning.

Example

The following example shows a HTTP request parameter that is used to construct a regular expression without sanitizing it first:

var express = require('express');
var app = express();

app.get('/findKey', function(req, res) {
  var key = req.param("key"), input = req.param("input");

  // BAD: Unsanitized user input is used to construct a regular expression
  var re = new RegExp("\\b" + key + "=(.*)\n");
});

Instead, the request parameter should be sanitized first, for example using the function _.escapeRegExp from the lodash package. This ensures that the user cannot insert characters which have a special meaning in regular expressions.

var express = require('express');
var _ = require('lodash');
var app = express();

app.get('/findKey', function(req, res) {
  var key = req.param("key"), input = req.param("input");

  // GOOD: User input is sanitized before constructing the regex
  var safeKey = _.escapeRegExp(key);
  var re = new RegExp("\\b" + safeKey + "=(.*)\n");
});

References

javascript/ql/src/Security/CWE-770/ResourceExhaustion.qhelp

Resource exhaustion

Applications are constrained by how many resources they can make use of. Failing to respect these constraints may cause the application to be unresponsive or crash. It is therefore problematic if attackers can control the sizes or lifetimes of allocated objects.

Recommendation

Ensure that attackers can not control object sizes and their lifetimes. If object sizes and lifetimes must be controlled by external parties, ensure you restrict the object sizes and lifetimes so that they are within acceptable ranges.

Example

The following example allocates a buffer with a user-controlled size.

var http = require("http"),
    url = require("url");

var server = http.createServer(function(req, res) {
	var size = parseInt(url.parse(req.url, true).query.size);

	let buffer = Buffer.alloc(size); // BAD

	// ... use the buffer
});

This is problematic since an attacker can choose a size that makes the application run out of memory. Even worse, in older versions of Node.js, this could leak confidential memory. To prevent such attacks, limit the buffer size:

var http = require("http"),
    url = require("url");

var server = http.createServer(function(req, res) {
	var size = parseInt(url.parse(req.url, true).query.size);

	if (size > 1024) {
		res.statusCode = 400;
		res.end("Bad request.");
		return;
	}

	let buffer = Buffer.alloc(size); // GOOD

	// ... use the buffer
});

Example

As another example, consider an application that allocates an array with a user-controlled size, and then fills it with values:

var http = require("http"),
    url = require("url");

var server = http.createServer(function(req, res) {
	var size = parseInt(url.parse(req.url, true).query.size);

	let dogs = new Array(size).fill("dog"); // BAD

	// ... use the dog
});

The allocation of the array itself is not problematic since arrays are allocated sparsely, but the subsequent filling of the array will take a long time, causing the application to be unresponsive, or even run out of memory. Again, a limit on the size will prevent the attack:

var http = require("http"),
    url = require("url");

var server = http.createServer(function(req, res) {
	var size = parseInt(url.parse(req.url, true).query.size);

	if (size > 1024) {
		res.statusCode = 400;
		res.end("Bad request.");
		return;
	}

	let dogs = new Array(size).fill("dog"); // GOOD

	// ... use the dogs
});

Example

Finally, the following example lets a user choose a delay after which a function is executed:

var http = require("http"),
    url = require("url");

var server = http.createServer(function(req, res) {
	var delay = parseInt(url.parse(req.url, true).query.delay);

	setTimeout(f, delay); // BAD

});

This is problematic because a large delay essentially makes the application wait indefinitely before executing the function. Repeated registrations of such delays will therefore use up all of the memory in the application. A limit on the delay will prevent the attack:

var http = require("http"),
    url = require("url");

var server = http.createServer(function(req, res) {
	var delay = parseInt(url.parse(req.url, true).query.delay);

	if (delay > 1000) {
		res.statusCode = 400;
		res.end("Bad request.");
		return;
	}

	setTimeout(f, delay); // GOOD

});

References

javascript/ql/src/Security/CWE-776/XmlBomb.qhelp

XML internal entity expansion

Parsing untrusted XML files with a weakly configured XML parser may be vulnerable to denial-of-service (DoS) attacks exploiting uncontrolled internal entity expansion.

In XML, so-called internal entities are a mechanism for introducing an abbreviation for a piece of text or part of a document. When a parser that has been configured to expand entities encounters a reference to an internal entity, it replaces the entity by the data it represents. The replacement text may itself contain other entity references, which are expanded recursively. This means that entity expansion can increase document size dramatically.

If untrusted XML is parsed with entity expansion enabled, a malicious attacker could submit a document that contains very deeply nested entity definitions, causing the parser to take a very long time or use large amounts of memory. This is sometimes called an XML bomb attack.

Recommendation

The safest way to prevent XML bomb attacks is to disable entity expansion when parsing untrusted data. How this is done depends on the library being used. Note that some libraries, such as recent versions of libxmljs (though not its SAX parser API), disable entity expansion by default, so unless you have explicitly enabled entity expansion, no further action is needed.

Example

The following example uses the XML parser provided by the node-expat package to parse a string xmlSrc. If that string is from an untrusted source, this code may be vulnerable to a DoS attack, since node-expat expands internal entities by default:

const app = require("express")(),
  expat = require("node-expat");

app.post("upload", (req, res) => {
  let xmlSrc = req.body,
    parser = new expat.Parser();
  parser.on("startElement", handleStart);
  parser.on("text", handleText);
  parser.write(xmlSrc);
});

At the time of writing, node-expat does not provide a way of controlling entity expansion, but the example could be rewritten to use the sax package instead, which only expands standard entities such as &amp;:

const app = require("express")(),
  sax = require("sax");

app.post("upload", (req, res) => {
  let xmlSrc = req.body,
    parser = sax.parser(true);
  parser.onopentag = handleStart;
  parser.ontext = handleText;
  parser.write(xmlSrc);
});

References

javascript/ql/src/Security/CWE-807/ConditionalBypass.qhelp

User-controlled bypass of security check

Using user-controlled data in a permissions check may allow a user to gain unauthorized access to protected functionality or data.

Recommendation

When checking whether a user is authorized for a particular activity, do not use data that is entirely controlled by that user in the permissions check. If necessary, always validate the input, ideally against a fixed list of expected values.

Similarly, do not decide which permission to check for, based on user data. In particular, avoid using computation to decide which permissions to check for. Use fixed permissions for particular actions, rather than generating the permission to check for.

Example

In this example, we have a server that shows private information for a user, based on the request parameter userId. For privacy reasons, users may only view their own private information, so the server checks that the request parameter userId matches a cookie value for the user who is logged in.

var express = require('express');
var app = express();
// ...
app.get('/full-profile/:userId', function(req, res) {

    if (req.cookies.loggedInUserId !== req.params.userId) {
        // BAD: login decision made based on user controlled data
        requireLogin();
    } else {
        // ... show private information
    }

});

This security check is, however, insufficient since an attacker can craft his cookie values to match those of any user. To prevent this, the server can cryptographically sign the security critical cookie values:

var express = require('express');
var app = express();
// ...
app.get('/full-profile/:userId', function(req, res) {

    if (req.signedCookies.loggedInUserId !== req.params.userId) {
        // GOOD: login decision made based on server controlled data
        requireLogin();
    } else {
        // ... show private information
    }

});

References

  • Common Weakness Enumeration: CWE-807.
  • Common Weakness Enumeration: CWE-290.
javascript/ql/src/Security/CWE-915/PrototypePollutingAssignment.qhelp

Prototype-polluting assignment

Most JavaScript objects inherit the properties of the built-in Object.prototype object. Prototype pollution is a type of vulnerability in which an attacker is able to modify Object.prototype. Since most objects inherit from the compromised Object.prototype object, the attacker can use this to tamper with the application logic, and often escalate to remote code execution or cross-site scripting.

One way to cause prototype pollution is by modifying an object obtained via a user-controlled property name. Most objects have a special __proto__ property that refers to Object.prototype. An attacker can abuse this special property to trick the application into performing unintended modifications of Object.prototype.

Recommendation

Use an associative data structure that is resilient to untrusted key values, such as a Map. In some cases, a prototype-less object created with Object.create(null) may be preferable.

Alternatively, restrict the computed property name so it can't clash with a built-in property, either by prefixing it with a constant string, or by rejecting inputs that don't conform to the expected format.

Example

In the example below, the untrusted value req.params.id is used as the property name req.session.todos[id]. If a malicious user passes in the ID value __proto__, the variable todo will then refer to Object.prototype. Finally, the modification of todo then allows the attacker to inject arbitrary properties onto Object.prototype.

let express = require('express');
let app = express()

app.put('/todos/:id', (req, res) => {
    let id = req.params.id;
    let items = req.session.todos[id];
    if (!items) {
        items = req.session.todos[id] = {};
    }
    items[req.query.name] = req.query.text;
    res.end(200);
});

One way to fix this is to use Map objects to associate key/value pairs instead of regular objects, as shown below:

let express = require('express');
let app = express()

app.put('/todos/:id', (req, res) => {
    let id = req.params.id;
    let items = req.session.todos.get(id);
    if (!items) {
        items = new Map();
        req.sessions.todos.set(id, items);
    }
    items.set(req.query.name, req.query.text);
    res.end(200);
});

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-020/UntrustedDataToExternalAPI.qhelp

Untrusted data passed to external API with additional heuristic sources

Using unsanitized untrusted data in an external API can cause a variety of security issues. This query reports external APIs that use untrusted data. The results are not filtered so that you can audit all examples. The query provides data for security reviews of the application and you can also use it to identify external APIs that should be modeled as either taint steps, or sinks for specific problems.

An external API is defined as a method call to a method that is not defined in the source code, not overridden in the source code, and is not modeled as a taint step in the default taint library. External APIs may be from the third-party dependencies or from internal dependencies. The query reports uses of untrusted data one of the arguments of external API call or in the return value from a callback passed to an external API.

Recommendation

For each result:

  • If the result highlights a known sink, confirm that the result is reported by the relevant query, or that the result is a false positive because this data is sanitized.
  • If the result highlights an unknown sink for a problem, then add modeling for the sink to the relevant query, and confirm that the result is either found, or is safe due to appropriate sanitization.
  • If the result represents a call to an external API that transfers taint, add the appropriate modeling, and re-run the query to determine what new results have appeared due to this additional modeling.
    Otherwise, the result is likely uninteresting. Custom versions of this query can extend the SafeExternalAPIMethod class to exclude known safe external APIs from future analysis.

Example

In this first example, a query parameter is read from the req parameter and then ultimately used in a call to the res.send external API:

express().get('/news', (req, res) => {
    let topic = req.query.topic;
    res.send(`<h1>${topic}</h1>`);
});

This is a reflected XSS sink. The XSS query should therefore be reviewed to confirm that this sink is appropriately modeled, and if it is, to confirm that the query reports this particular result, or that the result is a false positive due to some existing sanitization.

In this second example, again a query parameter is read from req.

let path = require('path');

express().get('/data', (req, res) => {
    let file = path.join(HOME_DIR, 'public', req.query.file);
    res.sendFile(file);
});

If the query reported the call to path.join on line 4, this would suggest that this external API is not currently modeled as a taint step in the taint tracking library. The next step would be to model this as a taint step, then re-run the query to determine what additional results might be found. In this example, it seems the result of the path.join will be used as a file path, leading to a path traversal vulnerability.

Note that both examples are correctly handled by the standard taint tracking library and security queries.

References

  • Common Weakness Enumeration: CWE-20.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-078/CommandInjection.qhelp

Uncontrolled command line with additional heuristic sources

Code that passes user input directly to require('child_process').exec, or some other library routine that executes a command, allows the user to execute malicious code.

Recommendation

If possible, use hard-coded string literals to specify the command to run or library to load. Instead of passing the user input directly to the process or library function, examine the user input and then choose among hard-coded string literals.

If the applicable libraries or commands cannot be determined at compile time, then add code to verify that the user input string is safe before using it.

Example

The following example shows code that takes a shell script that can be changed maliciously by a user, and passes it straight to child_process.exec without examining it first.

var cp = require("child_process"),
    http = require('http'),
    url = require('url');

var server = http.createServer(function(req<
8000
/span>, res) {
    let cmd = url.parse(req.url, true).query.path;

    cp.exec(cmd); // BAD
});

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-079/Xss.qhelp

Client-side cross-site scripting with additional heuristic sources

Directly writing user input (for example, a URL query parameter) to a webpage without properly sanitizing the input first, allows for a cross-site scripting vulnerability.

This kind of vulnerability is also called DOM-based cross-site scripting, to distinguish it from other types of cross-site scripting.

Recommendation

To guard against cross-site scripting, consider using contextual output encoding/escaping before writing user input to the page, or one of the other solutions that are mentioned in the references.

Example

The following example shows part of the page URL being written directly to the document, leaving the website vulnerable to cross-site scripting.

function setLanguageOptions() {
    var href = document.location.href,
        deflt = href.substring(href.indexOf("default=")+8);
    document.write("<OPTION value=1>"+deflt+"</OPTION>");
    document.write("<OPTION value=2>English</OPTION>");
}

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-089/SqlInjection.qhelp

Database query built from user-controlled sources with additional heuristic sources

If a database query (such as a SQL or NoSQL query) is built from user-provided data without sufficient sanitization, a malicious user may be able to run malicious database queries.

Recommendation

Most database connector libraries offer a way of safely embedding untrusted data into a query by means of query parameters or prepared statements.

For NoSQL queries, make use of an operator like MongoDB's $eq to ensure that untrusted data is interpreted as a literal value and not as a query object.

Example

In the following example, assume the function handler is an HTTP request handler in a web application, whose parameter req contains the request object.

The handler constructs two copies of the same SQL query involving user input taken from the request object, once unsafely using string concatenation, and once safely using query parameters.

In the first case, the query string query1 is built by directly concatenating a user-supplied request parameter with some string literals. The parameter may include quote characters, so this code is vulnerable to a SQL injection attack.

In the second case, the parameter is embedded into the query string query2 using query parameters. In this example, we use the API offered by the pg Postgres database connector library, but other libraries offer similar features. This version is immune to injection attacks.

const app = require("express")(),
      pg = require("pg"),
      pool = new pg.Pool(config);

app.get("search", function handler(req, res) {
  // BAD: the category might have SQL special characters in it
  var query1 =
    "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='" +
    req.params.category +
    "' ORDER BY PRICE";
  pool.query(query1, [], function(err, results) {
    // process results
  });

  // GOOD: use parameters
  var query2 =
    "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY=$1" + " ORDER BY PRICE";
  pool.query(query2, [req.params.category], function(err, results) {
    // process results
  });
});

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-094/CodeInjection.qhelp

Code injection with additional heuristic sources

Directly evaluating user input (for example, an HTTP request parameter) as code without properly sanitizing the input first allows an attacker arbitrary code execution. This can occur when user input is treated as JavaScript, or passed to a framework which interprets it as an expression to be evaluated. Examples include AngularJS expressions or JQuery selectors.

Recommendation

Avoid including user input in any expression which may be dynamically evaluated. If user input must be included, use context-specific escaping before including it. It is important that the correct escaping is used for the type of evaluation that will occur.

Example

The following example shows part of the page URL being evaluated as JavaScript code. This allows an attacker to provide JavaScript within the URL. If an attacker can persuade a user to click on a link to such a URL, the attacker can evaluate arbitrary JavaScript in the browser of the user to, for example, steal cookies containing session information.

eval(document.location.href.substring(document.location.href.indexOf("default=")+8))

The following example shows a Pug template being constructed from user input, allowing attackers to run arbitrary code via a payload such as #{global.process.exit(1)}.

const express = require('express')
var pug = require('pug');
const app = express()

app.post('/', (req, res) => {
    var input = req.query.username;
    var template = `
doctype
html
head
    title= 'Hello world'
body
    form(action='/' method='post')
        input#name.form-control(type='text)
        button.btn.btn-primary(type='submit') Submit
    p Hello `+ input
    var fn = pug.compile(template);
    var html = fn();
    res.send(html);
})

Below is an example of how to use a template engine without any risk of template injection. The user input is included via an interpolation expression #{username} whose value is provided as an option to the template, instead of being part of the template string itself:

const express = require('express')
var pug = require('pug');
const app = express()

app.post('/', (req, res) => {
    var input = req.query.username;
    var template = `
doctype
html
head
    title= 'Hello world'
body
    form(action='/' method='post')
        input#name.form-control(type='text)
        button.btn.btn-primary(type='submit') Submit
    p Hello #{username}`
    var fn = pug.compile(template);
    var html = fn({username: input});
    res.send(html);
})

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-117/LogInjection.qhelp

Log injection with additional heuristic sources

If unsanitized user input is written to a log entry, a malicious user may be able to forge new log entries.

Forgery can occur if a user provides some input with characters that are interpreted when the log output is displayed. If the log is displayed as a plain text file, then new line characters can be used by a malicious user. If the log is displayed as HTML, then arbitrary HTML may be included to spoof log entries.

Recommendation

User input should be suitably sanitized before it is logged.

If the log entries are in plain text then line breaks should be removed from user input, using String.prototype.replace or similar. Care should also be taken that user input is clearly marked in log entries.

For log entries that will be displayed in HTML, user input should be HTML-encoded before being logged, to prevent forgery and other forms of HTML injection.

Example

In the first example, a username, provided by the user, is logged using `console.info`. In the first case, it is logged without any sanitization. In the second case, the username is used to build an error that is logged using `console.error`. If a malicious user provides `username=Guest%0a[INFO]+User:+Admin%0a` as a username parameter, the log entry will be splitted in two different lines, where the second line will be `[INFO]+User:+Admin`.

const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
    let q = url.parse(req.url, true);

    console.info(`[INFO] User: ${q.query.username}`); // BAD: User input logged as-is
})

server.listen(3000, '127.0.0.1', () => {});

In the second example, String.prototype.replace is used to ensure no line endings are present in the user input.

const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
    let q = url.parse(req.url, true);

    // GOOD: remove newlines from user controlled input before logging
    let username = q.query.username.replace(/\n|\r/g, "");

    console.info(`[INFO] User: ${username}`);
});

server.listen(3000, '127.0.0.1', () => {});

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-134/TaintedFormatString.qhelp

Use of externally-controlled format string with additional heuristic sources

Functions like the Node.js standard library function util.format accept a format string that is used to format the remaining arguments by providing inline format specifiers. If the format string contains unsanitized input from an untrusted source, then that string may contain unexpected format specifiers that cause garbled output.

Recommendation

Either sanitize the input before including it in the format string, or use a %s specifier in the format string, and pass the untrusted data as corresponding argument.

Example

The following program snippet logs informa 6D40 tion about an unauthorized access attempt. The log message includes the user name, and the user's IP address is passed as an additional argument to console.log to be appended to the message:

const app = require("express")();

app.get("unauthorized", function handler(req, res) {
  let user = req.query.user;
  let ip = req.connection.remoteAddress;
  console.log("Unauthorized access attempt by " + user, ip);
});

However, if a malicious user provides %d as their user name, console.log will instead attempt to format the ip argument as a number. Since IP addresses are not valid numbers, the result of this conversion is NaN. The resulting log message will read "Unauthorized access attempt by NaN", missing all the information that it was trying to log in the first place.

Instead, the user name should be included using the %s specifier:

const app = require("express")();

app.get("unauthorized", function handler(req, res) {
  let user = req.query.user;
  let ip = req.connection.remoteAddress;
  console.log("Unauthorized access attempt by %s", user, ip);
});

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-346/CorsMisconfigurationForCredentials.qhelp

CORS misconfiguration for credentials transfer with additional heuristic sources

A server can send the "Access-Control-Allow-Credentials" CORS header to control when a browser may send user credentials in Cross-Origin HTTP requests.

When the Access-Control-Allow-Credentials header is "true", the Access-Control-Allow-Origin header must have a value different from "*" in order to make browsers accept the header. Therefore, to allow multiple origins for Cross-Origin requests with credentials, the server must dynamically compute the value of the "Access-Control-Allow-Origin" header. Computing this header value from information in the request to the server can therefore potentially allow an attacker to control the origins that the browser sends credentials to.

Recommendation

When the Access-Control-Allow-Credentials header value is "true", a dynamic computation of the Access-Control-Allow-Origin header must involve sanitization if it relies on user-controlled input.

Since the "null" origin is easy to obtain for an attacker, it is never safe to use "null" as the value of the Access-Control-Allow-Origin header when the Access-Control-Allow-Credentials header value is "true".

Example

In the example below, the server allows the browser to send user credentials in a Cross-Origin request. The request header origins controls the allowed origins for such a Cross-Origin request.

var https = require('https'),
    url = require('url');

var server = https.createServer(function(){});

server.on('request', function(req, res) {
    let origin = url.parse(req.url, true).query.origin;
     // BAD: attacker can choose the value of origin
    res.setHeader("Access-Control-Allow-Origin", origin);
    res.setHeader("Access-Control-Allow-Credentials", true);

    // ...
});

This is not secure, since an attacker can choose the value of the origin request header to make the browser send credentials to their own server. The use of a whitelist containing allowed origins for the Cross-Origin request fixes the issue:

var https = require('https'),
    url = require('url');

var server = https.createServer(function(){});

server.on('request', function(req, res) {
    let origin = url.parse(req.url, true).query.origin,
        whitelist = {
            "https://example.com": true,
            "https://subdomain.example.com": true,
            "https://example.com:1337": true
        };

    if (origin in whitelist) {
        // GOOD: the origin is in the whitelist
        res.setHeader("Access-Control-Allow-Origin", origin);
        res.setHeader("Access-Control-Allow-Credentials", true);
    }

    // ...
});

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-400/RemotePropertyInjection.qhelp

Remote property injection with additional heuristic sources

Dynamically computing object property names from untrusted input may have multiple undesired consequences. For example, if the property access is used as part of a write, an attacker may overwrite vital properties of objects, such as __proto__. This attack is known as prototype pollution attack and may serve as a vehicle for denial-of-service attacks. A similar attack vector, is to replace the toString property of an object with a primitive. Whenever toString is then called on that object, either explicitly or implicitly as part of a type coercion, an exception will be raised.

Moreover, if the name of an HTTP header is user-controlled, an attacker may exploit this to overwrite security-critical headers such as Access-Control-Allow-Origin or Content-Security-Policy.

Recommendation

The most common case in which prototype pollution vulnerabilities arise is when JavaScript objects are used for implementing map data structures. This case should be avoided whenever possible by using the ECMAScript 2015 Map instead. When this is not possible, an alternative fix is to prepend untrusted input with a marker character such as $, before using it in properties accesses. In this way, the attacker does not have access to built-in properties which do not start with the chosen character.

When using user input as part of a header name, a sanitization step should be performed on the input to ensure that the name does not clash with existing header names such as Content-Security-Policy.

Example

In the example below, the dynamically computed property prop is accessed on myObj using a user-controlled value.

var express = require('express');

var app = express();
var myObj = {}

app.get('/user/:id', function(req, res) {
	var prop = req.query.userControlled; // BAD
	myObj[prop] = function() {};
	console.log("Request object " + myObj);
});

This is not secure since an attacker may exploit this code to overwrite the property __proto__ with an empty function. If this happens, the concatenation in the console.log argument will fail with a confusing message such as "Function.prototype.toString is not generic". If the application does not properly handle this error, this scenario may result in a serious denial-of-service attack. The fix is to prepend the user-controlled string with a marker character such as $ which will prevent arbitrary property names from being overwritten.

var express = require('express');

var app = express();
var myObj = {}

app.get('/user/:id', function(req, res) {
	var prop = "$" + req.query.userControlled; // GOOD
	myObj[prop] = function() {};
	console.log("Request object " + myObj);
});

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-502/UnsafeDeserialization.qhelp

Deserialization of user-controlled data with additional heuristic sources

Deserializing untrusted data using any deserialization framework that allows the construction of arbitrary functions is easily exploitable and, in many cases, allows an attacker to execute arbitrary code.

Recommendation

Avoid deserialization of untrusted data if at all possible. If the architecture permits it, then use formats like JSON or XML that cannot represent functions. When using YAML or other formats that support the serialization and deserialization of functions, ensure that the parser is configured to disable deserialization of arbitrary functions.

Example

The following example calls the load function of the popular js-yaml package on data that comes from an HTTP request and hence is inherently unsafe.

const app = require("express")(),
  jsyaml = require("js-yaml");

app.get("load", function(req, res) {
  let data = jsyaml.load(req.params.data);
  // ...
});

Using the safeLoad function instead (which does not deserialize YAML-encoded functions) removes the vulnerability.

const app = require("express")(),
  jsyaml = require("js-yaml");

app.get("load", function(req, res) {
  let data = jsyaml.safeLoad(req.params.data);
  // ...
});

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-611/Xxe.qhelp

XML external entity expansion with additional heuristic sources

Parsing untrusted XML files with a weakly configured XML parser may lead to an XML External Entity (XXE) attack. This type of attack uses external entity references to access arbitrary files on a system, carry out denial-of-service (DoS) attacks, or server-side request forgery. Even when the result of parsing is not returned to the user, DoS attacks are still possible and out-of-band data retrieval techniques may allow attackers to steal sensitive data.

Recommendation

The easiest way to prevent XXE attacks is to disable external entity handling when parsing untrusted data. How this is done depends on the library being used. Note that some libraries, such as recent versions of libxml, disable entity expansion by default, so unless you have explicitly enabled entity expansion, no further action needs to be taken.

Example

The following example uses the libxml XML parser to parse a string xmlSrc. If that string is from an untrusted source, this code may be vulnerable to an XXE attack, since the parser is invoked with the noent option set to true:

const app = require("express")(),
  libxml = require("libxmljs");

app.post("upload", (req, res) => {
  let xmlSrc = req.body,
    doc = libxml.parseXml(xmlSrc, { noent: true });
});

To guard against XXE attacks, the noent option should be omitted or set to false. This means that no entity expansion is undertaken at all, not even for standard internal entities such as &amp; or &gt;. If desired, these entities can be expanded in a separate step using utility functions provided by libraries such as underscore, lodash or he.

const app = require("express")(),
  libxml = require("libxmljs");

app.post("upload", (req, res) => {
  let xmlSrc = req.body,
    doc = libxml.parseXml(xmlSrc);
});

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-643/XpathInjection.qhelp

XPath injection with additional heuristic sources

If an XPath expression is built using string concatenation, and the components of the concatenation include user input, it makes it very easy for a user to create a malicious XPath expression.

Recommendation

If user input must be included in an XPath expression, either sanitize the data or use variable references to safely embed it without altering the structure of the expression.

Example

In this example, the code accepts a user name specified by the user, and uses this unvalidated and unsanitized value in an XPath expression constructed using the xpath package. This is vulnerable to the user providing special characters or string sequences that change the meaning of the XPath expression to search for different values.

const express = require('express');
const xpath = require('xpath');
const app = express();

app.get('/some/route', function(req, res) {
  let userName = req.param("userName");

  // BAD: Use user-provided data directly in an XPath expression
  let badXPathExpr = xpath.parse("//users/user[login/text()='" + userName + "']/home_dir/text()");
  badXPathExpr.select({
    node: root
  });
});

Instead, embed the user input using the variable replacement mechanism offered by xpath:

const express = require('express');
const xpath = require('xpath');
const app = express();

app.get('/some/route', function(req, res) {
  let userName = req.param("userName");

  // GOOD: Embed user-provided data using variables
  let goodXPathExpr = xpath.parse("//users/user[login/text()=$userName]/home_dir/text()");
  goodXPathExpr.select({
    node: root,
    variables: { userName: userName }
  });
});

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-730/RegExpInjection.qhelp

Regular expression injection with additional heuristic sources

Constructing a regular expression with unsanitized user input is dangerous as a malicious user may be able to modify the meaning of the expression. In particular, such a user may be able to provide a regular expression fragment that takes exponential time in the worst case, and use that to perform a Denial of Service attack.

Recommendation

Before embedding user input into a regular expression, use a sanitization function such as lodash's _.escapeRegExp to escape meta-characters that have special meaning.

Example

The following example shows a HTTP request parameter that is used to construct a regular expression without sanitizing it first:

var express = require('express');
var app = express();

app.get('/findKey', function(req, res) {
  var key = req.param("key"), input = req.param("input");

  // BAD: Unsanitized user input is used to construct a regular expression
  var re = new RegExp("\\b" + key + "=(.*)\n");
});

Instead, the request parameter should be sanitized first, for example using the function _.escapeRegExp from the lodash package. This ensures that the user cannot insert characters which have a special meaning in regular expressions.

var express = require('express');
var _ = require('lodash');
var app = express();

app.get('/findKey', function(req, res) {
  var key = req.param("key"), input = req.param("input");

  // GOOD: User input is sanitized before constructing the regex
  var safeKey = _.escapeRegExp(key);
  var re = new RegExp("\\b" + safeKey + "=(.*)\n");
});

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-770/ResourceExhaustion.qhelp

Resource exhaustion with additional heuristic sources

Applications are constrained by how many resources they can make use of. Failing to respect these constraints may cause the application to be unresponsive or crash. It is therefore problematic if attackers can control the sizes or lifetimes of allocated objects.

Recommendation

Ensure that attackers can not control object sizes and their lifetimes. If object sizes and lifetimes must be controlled by external parties, ensure you restrict the object sizes and lifetimes so that they are within acceptable ranges.

Example

The following example allocates a buffer with a user-controlled size.

var http = require("http"),
    url = require("url");

var server = http.createServer(function(req, res) {
	var size = parseInt(url.parse(req.url, true).query.size);

	let buffer = Buffer.alloc(size); // BAD

	// ... use the buffer
});

This is problematic since an attacker can choose a size that makes the application run out of memory. Even worse, in older versions of Node.js, this could leak confidential memory. To prevent such attacks, limit the buffer size:

var http = require("http"),
    url = require("url");

var server = http.createServer(function(req, res) {
	var size = parseInt(url.parse(req.url, true).query.size);

	if (size > 1024) {
		res.statusCode = 400;
		res.end("Bad request.");
		return;
	}

	let buffer = Buffer.alloc(size); // GOOD

	// ... use the buffer
});

Example

As another example, consider an application that allocates an array with a user-controlled size, and then fills it with values:

var http = require("http"),
    url = require("url");

var server = http.createServer(function(req, res) {
	var size = parseInt(url.parse(req.url, true).query.size);

	let dogs = new Array(size).fill("dog"); // BAD

	// ... use the dog
});

The allocation of the array itself is not problematic since arrays are allocated sparsely, but the subsequent filling of the array will take a long time, causing the application to be unresponsive, or even run out of memory. Again, a limit on the size will prevent the attack:

var http = require("http"),
    url = require("url");

var server = http.createServer(function(req, res) {
	var size = parseInt(url.parse(req.url, true).query.size);

	if (size > 1024) {
		res.statusCode = 400;
		res.end("Bad request.");
		return;
	}

	let dogs = new Array(size).fill("dog"); // GOOD

	// ... use the dogs
});

Example

Finally, the following example lets a user choose a delay after which a function is executed:

var http = require("http"),
    url = require("url");

var server = http.createServer(function(req, res) {
	var delay = parseInt(url.parse(req.url, true).query.delay);

	setTimeout(f, delay); // BAD

});

This is problematic because a large delay essentially makes the application wait indefinitely before executing the function. Repeated registrations of such delays will therefore use up all of the memory in the application. A limit on the delay will prevent the attack:

var http = require("http"),
    url = require("url");

var server = http.createServer(function(req, res) {
	var delay = parseInt(url.parse(req.url, true).query.delay);

	if (delay > 1000) {
		res.statusCode = 400;
		res.end("Bad request.");
		return;
	}

	setTimeout(f, delay); // GOOD

});

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-776/XmlBomb.qhelp

XML internal entity expansion with additional heuristic sources

Parsing untrusted XML files with a weakly configured XML parser may be vulnerable to denial-of-service (DoS) attacks exploiting uncontrolled internal entity expansion.

In XML, so-called internal entities are a mechanism for introducing an abbreviation for a piece of text or part of a document. When a parser that has been configured to expand entities encounters a reference to an internal entity, it replaces the entity by the data it represents. The replacement text may itself contain other entity references, which are expanded recursively. This means that entity expansion can increase document size dramatically.

If untrusted XML is parsed with entity expansion enabled, a malicious attacker could submit a document that contains very deeply nested entity definitions, causing the parser to take a very long time or use large amounts of memory. This is sometimes called an XML bomb attack.

Recommendation

The safest way to prevent XML bomb attacks is to disable entity expansion when parsing untrusted data. How this is done depends on the library being used. Note that some libraries, such as recent versions of libxmljs (though not its SAX parser API), disable entity expansion by default, so unless you have explicitly enabled entity expansion, no further action is needed.

Example

The following example uses the XML parser provided by the node-expat package to parse a string xmlSrc. If that string is from an untrusted source, this code may be vulnerable to a DoS attack, since node-expat expands internal entities by default:

const app = require("express")(),
  expat = require("node-expat");

app.post("upload", (req, res) => {
  let xmlSrc = req.body,
    parser = new expat.Parser();
  parser.on("startElement", handleStart);
  parser.on("text", handleText);
  parser.write(xmlSrc);
});

At the time of writing, node-expat does not provide a way of controlling entity expansion, but the example could be rewritten to use the sax package instead, which only expands standard entities such as &amp;:

const app = require("express")(),
  sax = require("sax");

app.post("upload", (req, res) => {
  let xmlSrc = req.body,
    parser = sax.parser(true);
  parser.onopentag = handleStart;
  parser.ontext = handleText;
  parser.write(xmlSrc);
});

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-807/ConditionalBypass.qhelp

User-controlled bypass of security check with additional heuristic sources

Using user-controlled data in a permissions check may allow a user to gain unauthorized access to protected functionality or data.

Recommendation

When checking whether a user is authorized for a particular activity, do not use data that is entirely controlled by that user in the permissions check. If necessary, always validate the input, ideally against a fixed list of expected values.

Similarly, do not decide which permission to check for, based on user data. In particular, avoid using computation to decide which permissions to check for. Use fixed permissions for particular actions, rather than generating the permission to check for.

Example

In this example, we have a server that shows private information for a user, based on the request parameter userId. For privacy reasons, users may only view their own private information, so the server checks that the request parameter userId matches a cookie value for the user who is logged in.

var express = require('express');
var app = express();
// ...
app.get('/full-profile/:userId', function(req, res) {

    if (req.cookies.loggedInUserId !== req.params.userId) {
        // BAD: login decision made based on user controlled data
        requireLogin();
    } else {
        // ... show private information
    }

});

This security check is, however, insufficient since an attacker can craft his cookie values to match those of any user. To prevent this, the server can cryptographically sign the security critical cookie values:

var express = require('express');
var app = express();
// ...
app.get('/full-profile/:userId', function(req, res) {

    if (req.signedCookies.loggedInUserId !== req.params.userId) {
        // GOOD: login decision made based on server controlled data
        requireLogin();
    } else {
        // ... show private information
    }

});

References

  • Common Weakness Enumeration: CWE-807.
  • Common Weakness Enumeration: CWE-290.
javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-915/PrototypePollutingAssignment.qhelp

Prototype-polluting assignment with additional heuristic sources

Most JavaScript objects inherit the properties of the built-in Object.prototype object. Prototype pollution is a type of vulnerability in which an attacker is able to modify Object.prototype. Since most objects inherit from the compromised Object.prototype object, the attacker can use this to tamper with the application logic, and often escalate to remote code execution or cross-site scripting.

One way to cause prototype pollution is by modifying an object obtained via a user-controlled property name. Most objects have a special __proto__ property that refers to Object.prototype. An attacker can abuse this special property to trick the application into performing unintended modifications of Object.prototype.

Recommendation

Use an associative data structure that is resilient to untrusted key values, such as a Map. In some cases, a prototype-less object created with Object.create(null) may be preferable.

Alternatively, restrict the computed property name so it can't clash with a built-in property, either by prefixing it with a constant string, or by rejecting inputs that don't conform to the expected format.

Example

In the example below, the untrusted value req.params.id is used as the property name req.session.todos[id]. If a malicious user passes in the ID value __proto__, the variable todo will then refer to Object.prototype. Finally, the modification of todo then allows the attacker to inject arbitrary properties onto Object.prototype.

let express = require('express');
let app = express()

app.put('/todos/:id', (req, res) => {
    let id = req.params.id;
    let items = req.session.todos[id];
    if (!items) {
        items = req.session.todos[id] = {};
    }
    items[req.query.name] = req.query.text;
    res.end(200);
});

One way to fix this is to use Map objects to associate key/value pairs instead of regular objects, as shown below:

let express = require('express');
let app = express()

app.put('/todos/:id', (req, res) => {
    let id = req.params.id;
    let items = req.session.todos.get(id);
    if (!items) {
        items = new Map();
        req.sessions.todos.set(id, items);
    }
    items.set(req.query.name, req.query.text);
    res.end(200);
});

References

@erik-krogh erik-krogh removed WIP This is a work-in-progress, do not merge yet! Awaiting evaluation Do not merge yet, this PR is waiting for an evaluation to finish labels Dec 19, 2022
@erik-krogh erik-krogh marked this pull request as ready for review December 19, 2022 11:49
@erik-krogh erik-krogh requested review from a team as code owners December 19, 2022 11:49
@erik-krogh erik-krogh requested a review from aibaars December 19, 2022 11:49
Copy link
Contr 10000 ibutor
@aibaars aibaars left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

import DataFlow::PathGraph

/**
* Holds if the value of `nd` flows into `guard`.
Copy link
Contributor
@aibaars aibaars Dec 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only query in the heuristics folder with non-trivial contents. Would it be possible to move the predicates into a shared file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and there already is a ConditionalBypassQuery.qll file where these predicates should have been.

import DataFlow::PathGraph
import semmle.javascript.heuristics.AdditionalSources

from Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering whether you could move the entire select statement to shared query predicate problems() . If so, you could write:

import javascript
import semmle.javascript.security.dataflow.ExternalAPIUsedWithUntrustedDataQuery as Query
import DataFlow::PathGraph
import semmle.javascript.heuristics.AdditionalSources

query predicate problems(DataFlow::PathNode a, DataFlow::PathNode b, DataFlow::PathNode c, string d, DataFlow::PathNode e, string f) {
  Query::problems(a,b,c,d,e,f) and b.getNode() instanceof HeuristicSource
}

Not sure if it is an improvement though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's an improvement, so I prefer not to do that.
I tried to keep the modifications to the ordinary queries as small as possible, so that they wouldn't be harder to read.

ConditionalBypass.ql could use a Query.qll file though, so I'll get on that.

@erik-krogh erik-krogh added the no-change-note-required This PR does not need a change note label Dec 20, 2022
@erik-krogh erik-krogh requested a review from aibaars December 20, 2022 17:20
@erik-krogh erik-krogh merged commit cedc9c0 into github:main Jan 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
0