How to search and fix injection flaws

Injection flaws such as SQL Injection, Command injection, NoSQL Injection and even XSS (Cross-Site Scripting) all manifest the same in code. Namely, they combine user input (untrusted) with structured trusted input. This can be in the form of a simple string concatenation, but can also occur when working with buffers, and unsanitized replacements.

Searching for untrusted input

We label all expressions that have unknown values at compile time as untrusted. In practice these are mutable fields, method invocations, parameters, ... .

We can search for expressions that contain these untrusted sources. The expressions that contain the most risk are usually the ones passed as arguments for method calls or constructors. Simply settings the containsUntrustedSources option to true for expressions will yield the desired behavior.

The following example uses this mechanism to detect Cross-Site Scripting vulnerabilities.

public void example(String name) {
    this.response("<html><body><p>Welcome " + name + "</p></body></html>");
}

The name parameter has been concatenated with the HTML string without proper encoding. This will result in a cross-site scripting vulnerability. A quick example of exploiting this would be to supply the following argument: <script>alert(0);</script>.

Detecting this issue is fairly simple, we'll search for a function where the first argument is of the type String and contains untrusted input.

search:
  methodcall:
    name: "response"
    args:
      1:
        containsUntrustedInput: true

However, not everything should be considered untrusted. Invoking an encoding routine is syntax wise a method call. We can alter the behavior of the analysis by adding the option trustedSources to our recipe. This gives us the option to specify which unknowns are considered trusted.

The following example alters the previous recipe to consider all method calls with the name encode as trusted.

search:
  methodcall:
    name: "response"
    args:
      1:
        containsUntrustedInput: true
        trustedSources:
        - methodcall:
            name: "encode"

Writing suggestions to fix these mistakes

These mistakes are solved in two distinct manners, by sanitizing the input or by extracting the untrusted data.

Sanitizing input

In code, this means passing the original expression through a sanitization routine and use that result instead. An example of such an operation can be seen in the following example.

public void example(String name) {
    this.response("<html><body><p>Welcome " + encode(name) + "</p></body></html>");
}

Here we see that the untrusted parameter name has been used as an argument for the routine encode and that we concatenate the result of this function with the action HTML string.

To create such a suggestion we'll use a technique that's been mentioned before. By default, the action operates on the matched element. But this behavior can be changed by using the target option. If you have used containsUntrustedSources during the search part of a recipe, you will have the ability to choose untrusted as your desired target.

Since we now can operate on these untrusted elements, a simple rewrite will be sufficient to finalize our example.

availableFixes:
- name: "Encode untrusted input"
  actions:
  - rewrite:
      to: "encode({{{ . }}})"
      target: "untrusted"

Extracting untrusted data

Most APIs that work with a query language will provide mechanisms to separate your query and your data. A practical example would be the following scenario:

public void example(String firstname, String lastname) {
    this.query("SELECT * FROM users WHERE firstname = ? AND lastname = ?", firstname, lastname)
}

Here we can see that the query itself is passed as the first argument, and the parameters are passed as the remaining arguments. To indicate the API where we want our variables to be used, we use a ? as our placeholder.

Such an operation - in a security context - is usually called parameterization hence the name of the action parameterize.

An example configuration would be the following:

availableFixes:
- name: "Parameterize untrusted input"
  actions:
  - parameterize:
      placeholderFormat: "?"
      extractUntrustedInput:
        array:
          type: "java.lang.String[]"
          atArgumentPosition: 2

We've used extractUntrustedInput to configure where we want our data to be placed. In our example, we've chosen to extract it to an array and pass that array as the second argument of our method call.

Import to know is that before performing this extraction, we will first take a look at the method signature. If the second parameter is defined as a varargs parameter (e.g. String... arguments) then the action won't create an array, but just pass the data as separate arguments.

Another nice feature is that this will work when the code is already partially correct. We'll try to detect the correct position of the untrusted element in the existing array and insert it at the appropriate index.

Lastly, most APIs require the placeholders to not be encapsulated with string tokens (' and "). Sensei will detect this when performing the action and automatically remove the string tokens that are defined before and after the untrusted expression.