Tip
All examples used are valid against the Microsoft Northwind Sample Database You can run the resulting SQL from the examples against Northwind in order to test and play with the SqlObjects library.
Security¶
Overview¶
Warning
TL;DR;
Use Parameterized Queries to be safe. This library has not been fully battle-tested against any attacks including SQL Injection yet. A great deal of effort was made to build this library with security in mind. However, modern ORMs have been in existence far longer and have been tested more thoroughly. Therefore, it is safest to use this library to produce Parameterized Queries that can be submitted to your chosen ORM along with a value object and leave the security to the ORM.
The intention is to fully pen-test this library at some point to verify its mitigations. After that this documentation will be updated. For now, it is highly recommended that you use Parameterized Queries. Use other features of this library at your own risk.
Security is of paramount importance especially when it comes to the data access layer. Attempting to exploit SQL injection vulnerabilities is one of the first vectors of attack utilized by adversaries. This library provides protection against SQL injection attacks in the following ways.
Parameterized Queries
Strongly-Typed Values
Protecting Interpolated Strings
Value Sanitizing
Attack Detection
Each of these security features is described in their respective sections below.
Parameterized Queries¶
This library provides full support for Parameterized Queries. Anywhere that it makes sense to add an explicit
value, you can instead provide parameters in the @paramName
format. The SQL code will be rendered using
parameter names without quotes so that it conforms to the expected parameterized SQL statement format.
Examples of using @paramName
are shown throughout this documentation. All examples of parameterized SQL
statements assume that you will provide the parameter value object to the .Build()
method (not recommended
except for Debugging purposes)
or the ORM (recommended).
Examples using value objects with parameterized SQL statements are provided on the following pages in this documentation:
You SHOULD NOT embed variables directly into hand-coded Scripts using the .Script()
method or
the .Literal()
method. Doing so circumvents protections provided by this library or your ORM.
Warning
NEVER do this
.Script($" ... {varFromInput} ...")
OR this
.Literal($" ... {varFromInput} ...")
OR any string concatenation equivalent in a .Script()
or .Literal()
method call. This is a security hole waiting to
be exploited.
INSTEAD USE:
Parameterized Queries (recommended)
Strongly-Typed Method Calls (recommended below)
TSQLLiteral
objects for interpolated strings (explained below but NOT recommended)
Strongly Typed Values¶
The Fluent API provided in this library offers strongly-typed method overloads for the following data types, which will render in a compatible way to the expected SQL Server data types below:
int
long
decimal
bool
char
DateTime
Guid
String
For example, the .IsEqualTo()
method offers the following overloads for the above data types:
IsEqualTo(int value);
IsEqualTo(long value);
IsEqualTo(decimal value);
IsEqualTo(bool value);
IsEqualTo(char value);
IsEqualTo(DateTime value);
IsEqualTo(Guid value);
IsEqualTo(string value);
You can provide hard-coded values or variables into these method overloads, or any of the many other methods with the same overloads, and not worry about it.
You may be wondering
What about thestring
overload?Surely we should not provide user input values into a string method
When using the string
method overloads provided by the Fluent API it will sanitize the value for you.
For example, if you use the .IsEqualTo(string value)
overload with an input variable as shown below, you
will see that it produces a sanitized value.
var inputVar = "'; sql attack here /*";
var sql = TSQL
.UPDATE("Categories").SET()
.COLUMN("CategoryName").IsEqualTo("Vegan")
.COLUMN("Description").IsEqualTo("Exclusively plant-based food")
.WHERE("CategoryName").IsEqualTo(inputVar)
.Build()
;
Console.WriteLine(sql);
Output:
UPDATE [Categories] SET
[CategoryName] = 'Vegan'
WHERE 1=1
AND [CategoryName] = '''; sql attack here /*'
You can see above that the attempted attack is not executable and will fail.
If you’re curious, it does this by wrapping the string value in a TSQLLiteral
object as explained in the
Interpolated Strings section below. However, the difference is that the string
overloaded methods in the
Fluent API do not cause the sanitization to fail by wrapping the value in quotes first as explained in the
Interpolated Strings section below. The Fluent API methods prevent that error so you don’t have to worry
about it.
Tip
Just use Parameterized Queries. It’s safer.
Interpolated Strings¶
If you really insist on embedding a variable inside of an interpolated string or through string concatenation,
which is strongly discouraged (you should be using Parameterized Queries) then put the interpolated string
inside of a manually created TSQLLiteral
object as shown below.
But first …
DO NOT DO THIS!
var inputVar = "'; sql attack here --";
var sql = TSQL
.UPDATE("Categories").SET()
.COLUMN("CategoryName").IsEqualTo("Vegan")
.Literal("[Description] = '" + inputVar + "'" + Environment.NewLine)
.WHERE("CategoryID").IsEqualTo(9)
.Build()
;
Console.WriteLine(sql);
Output:
UPDATE [Categories] SET
[CategoryName] = 'Vegan',
[Description] = ''; sql attack here /*' WHERE 1=1
AND [CategoryID] = 9
As you can see above the attack is successfully executed!
A better way …
A safer (yet tedious) way to do it is to plant the inputVar
in a TSQLLiteral
object. The TSQLLiteral
object takes care of sanitizing the string so it is safe to use in a SQL statement.
!! BUT YOU MUST NOT WRAP IT IN QUOTES !!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | var inputVar = "'; sql attack here --";
var literal = new TSQLLiteral
{
DataType = new TSQLDataType(SqlDbType.VarChar),
Value = inputVar
};
var sql = TSQL
.UPDATE("Categories").SET()
.COLUMN("CategoryName").IsEqualTo("Vegan")
.Literal("[Description] = " + literal + Environment.NewLine)
.WHERE("CategoryID").IsEqualTo(9)
.Build()
;
Console.WriteLine(sql);
|
Output:
UPDATE [Categories] SET
[CategoryName] = 'Vegan',
[Description] = '''; sql attack here /*'
WHERE 1=1
AND [CategoryID] = 9
Notice on line 13
that the literal
object concatenated into the string is not surrounded by
single quotes. The TSQLLiteral
object does the job of rendering the string with quotes for you.
If you wrap it in quotes then you will undo the sanitization performed by the TSQLLiteral
object
and expose the SQL to injection.
Warning
If you decide to use string interpolation or concatenation protected by a TSQLLiteral
object then
you MUST NOT place the variable in single quotes. The TSQLLiteral
object will render the
string with quotes for you.
Tip
Just don’t use string interpolation or string concatenation. Use Parameterized Queries instead. It’s safer.
Sanitization¶
You have the option to sanitize a value using this library before you involve it in any SQL code or Fluent API calls if you choose. If this interests you for any reason you can accomplish this with the security filter used by this library as explained below.
Create the
SQLSecurityFilter
object using the static factory method below.var securityFilter = SQLSecurityFactory.Create();Next, you sanitize the input value
// result will contain the sanitized string var result = securityFilter.SanitizeInput<string>(inputVar);
At this point you can do what you will with the sanitized string.
If you provide any other primitive data type to the SanitizeInput<T>(string val)
method, then it will return
a default value suitable for data type T
if the inputVar
string is not a valid value for that data type.
For example:
var inputVar = "X";
// result will be == 0 and be of data type int
var result = securityFilter.SanitizeInput<int>(inputVar);
If the string value is suitable for data type T
then it will return the value as data type T
.
var inputVar = 99;
// result will be == 99 and be of data type int
var result = securityFilter.SanitizeInput<int>(inputVar);
Attack Detection¶
The SQLSecurityFilter
object also provides a CheckInput<T>(string val)
method that returns a tuple of
type (bool, IEnumerable<string>)
. Here is the signature.
(bool, IEnumerable<string>) CheckInput<T>(string val);
The bool
value will be true
if the string val
is valid for type T
and false
if not.
The IEnumerable<string>
collection will contain the SQL Injection signatures that were detected in string val
if any were found. The Sanitize<T>(string val)
method does not check for any attack signatures. It simply
returns a value suitable for type T
, whereas the CheckInput<T>(string val)
method runs the value through
a number of SQL injection signature rules and returns false
if the value is not good along with the messages
showing which attack signatures it detected.
var securityFilter = SQLSecurityFactory.Create();
var (okay, warnings) = securityFilter.CheckInput<string>(inputVar);
if (!okay)
{
// Log warnings, send alert, publish event, or whatever ...
}
This can be used for “trip wire” type intrusion detection functionality where you can check input strings and log the signatures triggered by the security filter’s rules. Logs that include the IP address and other details can be sent to a monitoring system for alerts.
If you use this to check values on a public-internet-facing system that anyone can access, then you are likely to trigger many logs of attacks. This may or may not be useful since it is practically a 100% certainty that all publicly facing systems will indeed be attacked.
However, you may find it more useful to embed this type of intrusion detection within internal systems where an attack is more likely to be rare and therefore very alarming, making this feature incredibly useful. If you are concerned or even curious if there are roque employees, or that an adversary has quietly infiltrated your organization’s systems then this type of intrusion detection could be a way to help solidify your security posture in the spirit of Defense in Depth principles.