How can I parameterize an SQL table without vulnerability to SQL injection - c#

I'm writing a C# class library in which one of the features is the ability to create an empty data table that matches the schema of any existing table.
For example, this:
private DataTable RetrieveEmptyDataTable(string tableName)
{
var table = new DataTable() { TableName = tableName };
using var command = new SqlCommand($"SELECT TOP 0 * FROM {tableName}", _connection);
using SqlDataAdapter dataAdapter = new SqlDataAdapter(command);
dataAdapter.Fill(table);
return table;
}
The above code works, but it has a glaring security vulnerability: SQL injection.
My first instinct is to parameterize the query like so:
using var command = new SqlCommand("SELECT TOP 0 * FROM #tableName", _connection);
command.Parameters.AddWithValue("#tableName", tableName);
But this leads to the following exception:
Must declare the table variable "#tableName"
After a quick search on Stack Overflow I found this question, which recommends using my first approach (the one with sqli vulnerability). That doesn't help at all, so I kept searching and found this question, which says that the only secure solution would be to hard-code the possible tables. Again, this doesn't work for my class library which needs to work for arbitrary table names.
My question is this: how can I parameterize the table name without vulnerability to SQL injection?

An arbitrary table name still has to exist, so you can check first that it does:
IF EXISTS (SELECT 1 FROM sys.objects WHERE name = #TableName)
BEGIN
... do your thing ...
END
And further, if the list of tables you want to allow the user to select from is known and finite, or matches a specific naming convention (like dbo.Sales%), or belongs to a specific schema (like Reporting), you could add additional predicates to check for those.
This requires you to pass the table name in as a proper parameter, not concatenate or token-replace. (And please don't use AddWithValue() for anything, ever.)
Once your check that the object is real and valid has passed, then you will still have to build your SQL query dynamically, because you still won't be able to parameterize the table name. You still should apply QUOTENAME(), though, as I explain in these posts:
Protecting Yourself from SQL Injection in SQL Server - Part 1
Protecting Yourself from SQL Injection in SQL Server - Part 2
So the final code would be something like:
CREATE PROCEDURE dbo.SelectFromAnywhere
#TableName sysname
AS
BEGIN
IF EXISTS (SELECT 1 FROM sys.objects
WHERE name = #TableName)
BEGIN
DECLARE #sql nvarchar(max) = N'SELECT *
FROM ' + QUOTENAME(#TableName) + N';';
EXEC sys.sp_executesql #sql;
END
ELSE
BEGIN
PRINT 'Nice try, robot.';
END
END
GO
If you also want it to be in some defined list you can add
AND #TableName IN (N't1', N't2', …)
Or LIKE <some pattern> or join to sys.schemas or what have you.
Provided nobody has the rights to then modify the procedure to change the checks, there is no value you can pass to #TableName that will allow you to do anything malicious, other than maybe selecting from another table you didn’t expect because someone with too much access was able to create before calling the code. Replacing characters like -- or ; does not make this any safer.

You could pass the table name to the SQL Server to apply quotename() on it to properly quote it and subsequently only use the quoted name.
Something along the lines of:
...
string quotedTableName = null;
using (SqlCommand command = new SqlCommand("SELECT quotename(#tablename);", connection))
{
SqlParameter parameter = command.Parameters.Add("#tablename", System.Data.SqlDbType.NVarChar, 128 /* nvarchar(128) is (currently) equivalent to sysname which doesn't seem to exist in SqlDbType */);
parameter.Value = tableName;
object buff = command.ExecuteScalar();
if (buff != DBNull.Value
&& buff != null /* theoretically not possible since a FROM-less SELECT always returns a row */)
{
quotedTableName = buff.ToString();
}
}
if (quotedTableName != null)
{
using (SqlCommand command = new SqlCommand($"SELECT TOP 0 FROM { quotedTableName };", connection))
{
...
}
}
...
(Or do the dynamic part on SQL Server directly, also using quotename(). But that seems overly and unnecessary tedious, especially if you will do more than one operation on the table in different places.)

Aaron Bertrand's answer solved the problem, but a stored procedure is not useful for a class library that might interact with any database. Here is the way to write RetrieveEmptyDataTable (the method from my question) using his
answer:
private DataTable RetrieveEmptyDataTable(string tableName)
{
const string tableNameParameter = "#TableName";
var query =
" IF EXISTS (SELECT 1 FROM sys.objects\n" +
$" WHERE name = {tableNameParameter})\n" +
" BEGIN\n" +
" DECLARE #sql nvarchar(max) = N'SELECT TOP 0 * \n" +
$" FROM ' + QUOTENAME({tableNameParameter}) + N';';\n" +
" EXEC sys.sp_executesql #sql;\n" +
"END";
using var command = new SqlCommand(query, _connection);
command.Parameters.Add(tableNameParameter, SqlDbType.NVarChar).Value = tableName;
using SqlDataAdapter dataAdapter = new SqlDataAdapter(command);
var table = new DataTable() { TableName = tableName };
Connect();
dataAdapter.Fill(table);
Disconnect();
return table;
}

Related

How to fix SQL Injection Issue of truncation of table

Below is the line of code where I truncate table records. The table value is coming from the front end. In my Veracode scan, it is showing SQL injection. How can I avoid this? I cannot create a stored procedure as the connection string is dynamic where I need to truncate this table. Is there another approach?
SqlCommand cmd = connection.CreateCommand();
cmd.Transaction = transaction;
cmd.CommandText = "TRUNCATE TABLE " + tablename;
cmd.ExecuteNonQuery();
You need dynamic sql:
string sql = #"
DECLARE #SQL nvarchar(150);
SELECT #SQL = 'truncate table ' + quotename(table_name) + ';'
FROM information_schema.tables
WHERE table_name = #table;
EXEC(#SQL);";
using (var connection = new SqlConnection("connection string here"))
using (var cmd = new SqlCommand(sql, connection))
{
cmd.Transaction = transaction;
cmd.Parameters.Add("#table", SqlDbType.NVarChar, 128).Value = tablename;
connection.Open();
cmd.ExecuteNonQuery();
}
This is one of very few times dynamic SQL makes things more secure, rather than less. Even better, if you also maintain a special table in this database listing other tables users are allowed to truncate, and use that rather than information_schema to validate the name. The idea of letting users just truncate anything is kind of scary.
Parametrized or not, you can make it only a little more secured in this case. Never totally secured. For this you need
create table TruncMapping in DB where you store
id guid
statement varchar(300)
your data will look like
SOME-GUID-XXX-YYY, 'TRUNCATE TABLE TBL1'
In your front end use a listbox or combobox with text/value like "Customer Data"/"SOME-GUID-XXX-YYY"
In your code use ExecuteScalar to execute Select statement from TruncMapping where id = #1 , where id will be parameterized GUID from combo value
Execute your truncate command using ExecuteNonQuery as you do now but with a retrieved string from previous call.
Your scan tool will most likely choke. If it is still thinking code is unsafe, you can safely point this as false positive because what you execute is coming from your secured DB. Potential attacker has no way to sabotage your "non-tuncatable tables" because they are not listed in TruncMapping tables.
You've just created multi-layered defense against sql injection.
here is one way to hide it from scanning tools
private const string _sql = "VFJVTkNBVEUgVEFCTEU=";
. . . .
var temp = new { t = tablename };
cmd.CommandText =
Encoding.ASCII.GetString(Convert.FromBase64String(_sql)) + temp.t.PadLeft(temp.t.Length + 1);
security by obscurity

Syntax error with creating a stored procedure in SQL Server Management Studio

Currently, I am working on an inventory system that will allow the client to customize their own SQL Server tables.
The user will name their column then specify the data type they would like to use. After they are satisfied with their table they click on the submit table button to create their table. C# then will create a unique table for the user by running a SQL Server stored procedure.
SqlConnection connection = new SqlConnection(Constants.conn);
connection.Open();
string query = "EXEC CreateTable #TableName='"+ UserAccountInfo.Username + "InventoryTable'";
SqlCommand command = new SqlCommand(query, connection);
command.ExecuteReader();
Then using a foreach loop, iterating through each column added (class UserTable is my custom WPF UserControl) by looking at the Stack panel "StackTable" Children, and assigning the properties of the Usertable "Name" and "DataType" to the List of String Arrays. Then I iterate throughout the list and execute the second stored procedure that alters the table previously created.
I believe all of this to work!! My question and problem lies in my stored procedure.
List<string[]> list = new List<string[]>();
foreach (UserTable ut in StackTable.Children)
{
string dataType = "";
switch (ut.DataType.Text)
{
case "Text": dataType = "Varchar(100)"; break;
case "Number": dataType = "INT"; break;
case "Boolean": dataType = "Varchar(5)"; break;
}
string[] vs = {"[" + ut.Name.Text + "]", dataType};
list.Add(vs);
}
foreach (string[] s in list)
{
string query2 = "EXEC SelectAllCustomers #Name = '" + s[0] + "', #DataType = '" + s[1] + "';";
SqlCommand command2 = new SqlCommand(query, connection);
command2.ExecuteReader();
}
My DATABASE Schema consists of the base database called InventoryDatabase, inside a table called LoginInfo where I store the user's information, and the rest of the tables are going to be the tables we are creating above. PROBLEM IS, my stored procedures are throwing syntax errors.
You'll have to use dynamic SQL. Here's an example to get you started:
create or alter procedure CreateTable #TableName varchar(100)
as
begin
declare #sql nvarchar(max) =
concat('create table ',quotename(#TableName),' AccountId int primary key identity(1,1)');
exec (#sql);
end
Note that the name is passed through the quotename function to protect against SQL Injection.
Option 1: prepare the SQL script as a string which can able to Create or alter the table in your C# code and then execute prepared string using ExecuteNonQuery method.
Option 2: by using dynamic SQL concept, prepare the create/alter table script with in a stored procedure and then execute

Where In Clause error on The server support a maximum of 2100 parameters only

I'm currently trying to try catch the insert SQL query from my simple C# application, but when i'm trying to insert the data into database with user id which is added into userIDList parameter, the error came out saying that
The incoming request has too many parameters. The server supports a
maximum of 2100 parameters.
The userIDList sometimes will contains like 60 arrays above then the error will popped out.
My SQL CommandText will contain of
"SELECT * FROM TIME_ATTENDANCE_REPORT WHERE TRXDATETIME = #Date AND USERID IN (001,002,003,004,....)
So i think if more then certain number then the error popped out
Here are my sample code :
List<string> userIDList = new List<string>();
using (SqlCommand sqlDBComm = new SqlCommand())
{
openConnection();
SqlDataReader sqlDBReader;
sqlDBReader = null;
sqlDBComm.CommandText = "SELECT * FROM TIME_ATTENDANCE_REPORT WHERE TRXDATETIME = #Date AND USERID IN (" + string.Join(",", userIDList) + ") ORDER BY USERID ASC ";
sqlDBComm.Parameters.Add("#Date", SqlDbType.DateTime);
sqlDBComm.Parameters["#Date"].Value = GetDateFrom;
sqlDBComm.Connection = sqlDB;
sqlDBComm.CommandType = CommandType.Text;
try
{
sqlDBReader = sqlDBComm.ExecuteReader();
t.Load(sqlDBReader);
sqlDBReader.Close();
if (t.Rows.Count > 0)
{
status = "Update";
}
else
{
status = "Insert";
}
}
catch (Exception errMsg)
{
MessageBox.Show("Error Code: " + errMsg.ToString());
}
finally
{
sqlDBReader.Close();
closeConnection();
}
}
Any other solution can resolve this?
Thanks
There are many ways to solve this problem.
Instead of sending a list of IDs as seperate parameters, you can send a single #IDList parameter as a single comma separated string and let it parsed into IDs at the server side. Here is a function that I use for this (borrowed and modified from Jeff Moden's code):
CREATE FUNCTION [dbo].[iSplitter] (#Parameter VARCHAR(MAX))
RETURNS #splitResult TABLE (number INT, [value] INT)
AS
BEGIN
SET #Parameter = ','+#Parameter +',';
WITH cteTally AS
(
SELECT TOP (LEN(#Parameter))
ROW_NUMBER() OVER (ORDER BY t1.Object_ID) AS N
FROM Master.sys.All_Columns t1
CROSS JOIN Master.sys.All_Columns t2
)
INSERT #splitResult
SELECT ROW_NUMBER() OVER (ORDER BY N) AS Number,
SUBSTRING(#Parameter,N+1,CHARINDEX(',',#Parameter,N+1)-N-1) AS [Value]
FROM cteTally
WHERE N < LEN(#Parameter) AND SUBSTRING(#Parameter,N,1) = ','
RETURN
END
With this function created once, I do it like:
sqlDBComm.CommandText = #"SELECT * FROM TIME_ATTENDANCE_REPORT tar
inner Join dbo.iSplitter(#UserIdList) ul on tar.USERID = ul.[value]
WHERE TRXDATETIME = #Date
ORDER BY USERID ASC ";
sqlDBComm.Parameters.AddWithValue("#UserIdList",string.Join(",", userIDList));
This works very well for 5-6K integer ids but times out if used with 20-30K or more IDs. Then I created another alternative as a CLR procedure, and that one parses the list server side in less than a second. But I think this one is sufficient for your needs.
Another way is to send the IDs as an XML parameter and parse server side again.
Yet another way is to send a table parameter.
PS: Here is a link that shows sample code for other ways. The site is in Turkish but the codes are crystal clear in C#, separate per approach.
EDIT: XML sample using Northwind Orders table:
void Main()
{
int[] IDList = { 10265,10266,10267,10268,10269,10270,10271,10272,10273,10274,10275, 10320, 10400 };
var idsAsXML = new XElement("IDS",
from i in IDList
select new XElement("Row", new XAttribute("Id", i)));
string sql = #"
DECLARE #hDoc int;
DECLARE #tbl TABLE (Id int);
exec sp_xml_preparedocument #hDoc OUTPUT, #XML;
INSERT #tbl
SELECT *
FROM OPENXML(#hDoc, #Nodename, 1) WITH (Id int);
EXEC sp_xml_removedocument #hDoc;
select * from Orders o
where exists (select * from #tbl t where t.Id = o.OrderId) ";
DataTable tbl = new DataTable();
using (SqlConnection con = new SqlConnection(#"server=.\SQLExpress;Trusted_Connection=yes;Database=Northwind"))
{
SqlCommand cmd = new SqlCommand(sql, con);
cmd.Parameters.AddWithValue("#XML", idsAsXML.ToString());
cmd.Parameters.AddWithValue("#NodeName", "/IDS/Row");
con.Open();
tbl.Load(cmd.ExecuteReader());
con.Close();
}
//tbl.Dump(); // linqPad luxury
}
You can create a Table-Valued-Parameter and pass it as a parameter. It requires you to create a new type in your database, and will enable you to pass an array to the query and let the database treat it as a table. If this is something you do a lot, it could come in handy.
I no longer have access to the project where I implemented this but everything is available in the blog post. The code below is not tested, but I hope it can get you in the right direction.
1. Create a new type in your database:
CREATE TYPE integer_list_tbltype AS TABLE (n int NOT NULL PRIMARY KEY)
2. Pass it as parameter:
sqlDBComm.Parameters.Add("#userIds", SqlDbType.Structured)
sqlDBComm.Parameters["#userIds"].Direction = ParameterDirection.Input
sqlDBComm.Parameters["#userIds"].TypeName = "integer_list_tbltype"
sqlDBComm.Parameters["#userIds"].Value = CreateDataTable(userIDList)
3. Method for creating the parameter:
private static DataTable CreateDataTable(IEnumerable<int> ids) {
DataTable table = new DataTable();
table.Columns.Add("n", typeof(int));
foreach (int id in ids) {
table.Rows.Add(id);
}
return table;
}
4. Use it in your SQL:
... AND USERID IN (SELECT n FROM #userIds)
CreateDataTable from here:
How to pass table value parameters to stored procedure from .net code
Rest from here:
http://www.sommarskog.se/arrays-in-sql-2008.html#introduction

C# Using sql parameters which now won't work

Hey I was using parametrized queries for my application which worked just fine but now (I don't know why) they aren't replaced anymore with the values...
So instead of running something like "SELECT [TABLE_NAME] FROM [MyDefinetelyExistingDatabase]"; it tries to execute "SELECT [TABLE_NAME] FROM [#targetDatabase]"; which, of course, will fail.
var dataBaseToGetTablesFrom = "MyDefinetelyExistingDatabase";
var results = new List<string>();
const string query = #"SELECT
[TABLE_NAME] AS tableName
FROM
[#targetDatabase].[INFORMATION_SCHEMA].[TABLES] ;";
using (var context = new ConnectionHandler(true))
{
if (context.Connection.State != ConnectionState.Open)
throw new ConnectionFailedException(context.Connection.State);
using (var command = new SqlCommand(query, context.Connection))
{
command.Parameters.AddWithValue("#targetDatabase", dataBaseToGetTablesFrom);
using (var reader = command.ExecuteReader())
{
if (!reader.HasRows)
return results.ToArray();
while (reader.Read())
results.Add(reader.GetString(0));
}
}
}
return results.ToArray();
I now tried different formats and things to add the parameters but it results in the same...
I don't want to do this by inserting the values into the query directly via string.Format eg but I want to have those parameters (which work properly at different places in the code (???) but not where I want.
In fact, I need to use parameters in every statement and must be able to address different databases by calling them like [DB].[Table-Schema].[Table]
[EDIT]
Hey guys, figured the problem some days ago and thought I share it with you.
As far as I have noticed, my problem at the whole was to try to replace the databasename and / or in some other examples, the table name as well.
So this won't work which makes clearly sense to me as the server can't prepare to execute a statement if it doesn't even know on which table it should work and therefore doesn't know anything about the structure etc.
So I changed my statements to fit my new knowledge and it worked as expected like a charm.
I don't know what ConnectionHandler is, but if that is your own code you can implement it with SqlConnectionStringBuilder which will allow you to use a variable to assign the InitialCatalog instead of putting the database name in the query. This would be preferable to dynamic sql which requires careful sanitization.
You would need dynamic sql for this something like.....
DECLARE #Sql NVARCHAR(MAX);
SET #Sql = N' SELECT [TABLE_NAME] AS tableName '
+ N' FROM ' + QUOTENAME(#targetDatabase) + N'.[INFORMATION_SCHEMA].[TABLES]'
Exec sp_executesql #Sql

Delete with params in SqlCommand

I use ADO.NET to delete some data from DB like this:
using (SqlConnection conn = new SqlConnection(_connectionString))
{
try
{
conn.Open();
using (SqlCommand cmd = new SqlCommand("Delete from Table where ID in (#idList);", conn))
{
cmd.Parameters.Add("#idList", System.Data.SqlDbType.VarChar, 100);
cmd.Parameters["#idList"].Value = stratIds;
cmd.CommandTimeout = 0;
cmd.ExecuteNonQuery();
}
}
catch (Exception e)
{
//_logger.LogMessage(eLogLevel.ERROR, DateTime.Now, e.ToString());
}
finally
{
conn.Close();
}
}
That code executes without Exception but data wasn't deleted from DB.
When I use the same algorithm to insert or update DB everything is OK.
Does anybody know what is the problem?
You can't do that in regular TSQL, as the server treats #idList as a single value that happens to contain commas. However, if you use a List<int>, you can use dapper-dot-net, with
connection.Execute("delete from Table where ID in #ids", new { ids=listOfIds });
dapper figures out what you mean, and generates an appropriate parameterisation.
Another option is to send in a string and write a UDF to perform a "split" operation, then use that UDF in your query:
delete from Table where ID in (select Item from dbo.Split(#ids))
According to Marc's Split-UDF, this is one working implementation:
CREATE FUNCTION [dbo].[Split]
(
#ItemList NVARCHAR(MAX),
#delimiter CHAR(1)
)
RETURNS #IDTable TABLE (Item VARCHAR(50))
AS
BEGIN
DECLARE #tempItemList NVARCHAR(MAX)
SET #tempItemList = #ItemList
DECLARE #i INT
DECLARE #Item NVARCHAR(4000)
SET #tempItemList = REPLACE (#tempItemList, ' ', '')
SET #i = CHARINDEX(#delimiter, #tempItemList)
WHILE (LEN(#tempItemList) > 0)
BEGIN
IF #i = 0
SET #Item = #tempItemList
ELSE
SET #Item = LEFT(#tempItemList, #i - 1)
INSERT INTO #IDTable(Item) VALUES(#Item)
IF #i = 0
SET #tempItemList = ''
ELSE
SET #tempItemList = RIGHT(#tempItemList, LEN(#tempItemList) - #i)
SET #i = CHARINDEX(#delimiter, #tempItemList)
END
RETURN
END
And this is how you could call it:
DELETE FROM Table WHERE (ID IN (SELECT Item FROM dbo.Split(#idList, ',')));
I want to give this discussion a little more context. This seems to fall under the topic of "how do I get multiple rows of data to sql". In #Kate's case she is trying to DELETE-WHERE-IN, but useful strategies for this user case are very similar to strategies for UPDATE-FROM-WHERE-IN or INSERT INTO-SELECT FROM. The way I see it there are a few basic strategies.
String Concatenation
This is the oldest and most basic way. You do a simple "SELECT * FROM MyTable WHERE ID IN (" + someCSVString + ");"
Super simple
Easiest way to open yourself to a SQL Injection attack.
Effort you put into cleansing the string would be better spent on one of the other solutions
Object Mapper
As #MarcGravell suggested you can use something like dapper-dot-net, just as Linq-to-sql or Entity Framework would work. Dapper lets you do connection.Execute("delete from MyTable where ID in #ids", new { ids=listOfIds }); Similarly Linq would let you do something like from t in MyTable where myIntArray.Contains( t.ID )
Object mappers are great.
However, if your project is straight ADO this is a pretty serious change to accomplish a simple task.
CSV Split
In this strategy you pass a CSV string to SQL, whether ad-hoc or as a stored procedure parameter. The string is processed by a table valued UDF that returns the values as a single column table.
This has been a winning strategy since SQL-2000
#TimSchmelter gave a great example of a csv split function.
If you google this there are hundreds of articles examining every aspect from the basics to performance analysis across various string lengths.
Table Valued Parameters
In SQL 2008 custom "table types" can be defined. Once the table type is defined it can be constructed in ADO and passed down as a parameter.
The benefit here is it works for more scenarios than just an integer list -- it can support multiple columns
strongly typed
pull string processing back up to a layer/language that is quite good at it.
This is a fairly large topic, but Table-Valued Parameters in SQL Server 2008 (ADO.NET) is a good starting point.

Categories

Resources