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.