sp_executesql securing the dynamic search keywords - sql

I am trying to convert the SQL statement to support the sp_executesql to make it safe but I ran into an unsafe area. Hopefully you guys can help me with this. I've created the temp table to make it easier to demonstrate the problem.
The problem is at STEP # 6. I can use the STEP # 5 BUT this is not safe and it can be hacked easily. I don't really want break the keywords and search multiple times because of the system performance.
Error for MS SQL 2008 Msg 4145, Level 15, State 1, Line 4 An expression of non-boolean type specified in a context where a condition is expected, near 'ORDER'.
GO
/****** Object: StoredProcedure [dbo].[ups_MultiWareHouse] Script Date: 06/14/2012 09:12:38 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER OFF
GO
create PROCEDURE ups_TestSearch(
#Keywords nvarchar(4000),
#SortColumns nvarchar(4000)
)
AS
--STEP #1 - Create Temp Table - Begin
CREATE TABLE #TempTable
(
ProductID uniqueidentifier,
ProductName varchar(600),
Price decimal(18,2),
Active bit
)
--STEP #2 - Insert couple records to search
INSERT INTO #TempTable (ProductID,ProductName,Price,Active) VALUES(NEWID(),'Mouse','10.12','1')
INSERT INTO #TempTable (ProductID,ProductName,Price,Active) VALUES(NEWID(),'Keyboard','20.45','1')
INSERT INTO #TempTable (ProductID,ProductName,Price,Active) VALUES(NEWID(),'Monitor','150.87','0')--Disable this product
--STEP #3 - Display the current table data
select 'STEP #3' as STEP, * FROM #TempTable
--STEP #4 - SETTING UP sp_executesql to support parameter substitution
--Set definition
DECLARE #ParmDefinition nvarchar(4000);
SET #ParmDefinition='
#Param1ProductName nvarchar(4000),
#Param2SortColumns nvarchar(4000)
'
DECLARE #SQLString nvarchar(4000);
--STEP #5- CONVERT THE #SQLString TO use #Keywords and #SortColumns
--Run query for the below like this ups_TestSearch'ProductName=''Mouse'' OR ProductName=''Keyboard''', 'Price DESC, ProductName ASC'
SET #SQLString = N'SELECT ''STEP #5'' as STEP, #TempTable.* FROM #TempTable WHERE ('+#Keywords+') ORDER BY '+#SortColumns;--unsafe, open to hackers
EXECUTE sp_executesql #SQLString, #ParmDefinition, #Param1ProductName = #Keywords, #Param2SortColumns=#SortColumns;
--STEP #6- CONVERT THE #SQLString TO use #Keywords and #SortColumns
--Run query for the below like this ups_TestSearch'ProductName=''Mouse'' OR ProductName=''Keyboard''', 'Price DESC, ProductName ASC'
SET #SQLString = N'SELECT ''STEP #6'' as STEP, #TempTable.* FROM #TempTable WHERE (#Param1ProductName) ORDER BY #SortColumns';--Safe but not working
SELECT #SQLString AS SeeStatement
EXECUTE sp_executesql #SQLString, #ParmDefinition, #Param1ProductName = #Keywords, #Param2SortColumns=#SortColumns;
--Drop temp table
DROP TABLE #TempTable

I think the issue is that in Step 5 you aren't using parameter substitution - that is, you are basically building the SQL statement by string concatenation. When you execute it via sp_executesql you could really just do this:
EXECUTE sp_executesql #SqlString
The code in Step 6 is performing parameter substitution. In this case, however, you are limited to using parameters only in locations where they are allowed in "normal" SQL expressions. For example, you can't do this in T-SQL:
DECLARE #Criteria NVARCHAR(500);
SET #Criteria = N' WHERE ProductName = ''Mouse'''
SELECT * FROM #MyTempTable + #Criteria
Depending upon how complex you expect your filter to be, you might be able to write the criteria to a temporary table and perform a join to the temporary table to limit the resulting data that is returned. Off the top of my head I am not sure how best to sort the resulting data unless you did that in the calling code perhaps?

Your error message indicates that the WHERE clause in step 6 is invalid, and so is the ORDER BY clause. This is because you're passing in strings as parameters to sp_executesql and trying to use them as entire clauses. Additionally, the statement references a parameter #SortColumns, but you appear to have named the parameter #Param2SortColumns.
Have a read of what some SQL server MVPs have written:
http://www.sommarskog.se/dynamic_sql.html
More to the point: http://www.sommarskog.se/dyn-search.html
http://www.sqlmag.com/article/tsql3/parameterizing-result-order
I don't see a simple way to alter your procedure to make this work, since you're passing in entire WHERE and ORDER BY clauses as parameters. What you should really do is redesign the proc. Supply each WHERE criterion as an individual parameter to ups_TestSearch. You resupply each WHERE parameter to sp_executesql and structure your initial SQL statement in this fashion:
SET #SQLString = SELECT and JOIN portions of command
SET #SQLString = #SQLString + 'WHERE 1 = 1 '
IF (#WhereParam1 IS NOT NULL)
SET #SQLString = #SQLString + 'AND (SomeTable.SomeColumn = #WhereParam1) '
IF (#WhereParam2 IS NOT NULL)
SET #SQLString = #SQLString + 'AND (SomeTable.SomeColumn = #WhereParam2) '
...
If necessary, you can use the same structure to add joins to the statement.
The ORDER BY structure depends on how complex this might get, and whether you know all possible involved columns. If it's relatively simple, you can write it out as a CASE statement as follows, or break it up as individual parameters, as I recommend for the WHERE clause.
ORDER BY
CASE WHEN CHARINDEX(#SortColumns, 'SortCol1') > 0 THEN SortCol1 ELSE NULL END,
CASE WHEN CHARINDEX(#SortColumns, 'SortCol2') > 0 THEN SortCol2 ELSE NULL END,
...
The simplest thing to do here might be to sort at the application level rather than the DB, but that could be just as infeasible as a complex ORDER BY clause would be to parameterize.

Related

Scalar variable must be declared in SQL variable

I'm creating a report using sql scripts through management studio and I'm getting the error " Must Declare the scalar variable "#Account". I've been reading other similar questions on this portal but they are related to c#
I'm currently trying to reduce the code on the script so I decided to put a sql script into a variable because depending on a condition the where condition will change. Below is an example of the code
Declare #Account int = 1 , #SQL varchar(max)=''
Select #SQL = N'Select ColumnA,ColumnB, ColumnC from Table1 where ColumnA =1'
if #Account IS NULL
Begin
exec(#SQL)
end
--Here is where the error is hapening
else
begin
--This is the line causing the error
Select #SQL = #SQL + 'AND ColumnB=#Account"
exec(#SQL)
end
If I type manually the value of the variable next to "ColumnB=" it works but the account number will be selected by the user executing the script. I'm thinking on maybe building a temp table to capture the variable value and then do a sub query on the where condition but maybe the solution to this error may be more easier
You want sp_executesql:
select #SQL = #SQL + 'AND ColumnB=#Account';
exec sp_executesql #SQL, N'#Account int', #Account=#Account;
This is how you pass parameters into a dynamic SQL statement in SQL Server. I strongly recommend that you only use sp_executesql to execute SQL statements -- even when you don't have parameters. Using it makes it easy to implement parameters when you need them.
You are passing in '#Account' into the #SQL variable -- the underlying EXEC cannot see that variable.
One way of fixing this would instead be to do this:
Select #SQL = #SQL + 'AND ColumnB=' + CAST(#Account as varchar)

How do I use a parameter for my tablename in my sql procedure

I don't get any errors when I do this, but it creates a table called "dbo.#tablename" in my database when i really want is for it to create the value that i am passing as the parameter in the exec procedure as the tablename. What am i doing wrong. Here is my update procedure script. Maybe i can change so so that it does create the value as the table name.
Here is what i have so far:
ALTER PROCEDURE [dbo].[Load_Negatives]
-- Add the parameters for the stored procedure here
#TABLENAME SYSNAME,
#AuditPeriodStartDate datetime,
#AuditPeriodEndDate datetime
AS
BEGIN
SET NOCOUNT ON;
Select
Location,
Customer,
Transaction_date
into
dbo.[#TABLENAME]
from dbo.CustomerHistory (nolock)
where
[Transaction_date] between #AuditPeriodStartDate and #AuditPeriodEndDate
END
Table names cannot be parametrised. Therefore, you need to build your SQL statement dynamically, incorporating the name into the dynamic script.
To minimise the risk of SQL injection, use the QUOTENAME system function with the #TABLENAME value and introduce parametrisation to your dynamic query to pass the other two parameters of the stored procedure:
ALTER PROCEDURE [dbo].[Load_Negatives]
#TABLENAME SYSNAME,
#AuditPeriodStartDate datetime,
#AuditPeriodEndDate datetime
AS
BEGIN
SET NOCOUNT ON;
DECLARE #sql nvarchar(max);
SET #sql = N'Select
Location,
Customer,
Transaction_date
into
dbo.' + QUOTENAME(#TABLENAME) + N'
from dbo.CustomerHistory (nolock)
where
[Transaction_date] between #AuditPeriodStartDate and #AuditPeriodEndDate'
;
EXECUTE sp_executesql
#sql,
N'#AuditPeriodStartDate datetime, #AuditPeriodEndDate datetime',
#AuditPeriodStartDate, #AuditPeriodEndDate
;
END
Basically, the dynamic query looks almost exactly the same as your current query. The only difference is that the table name is added as the result of QUOTENAME(#TABLENAME). The datetime parameters of the dynamic query happen to have same names as the corresponding parameters of the stored procedure but that is not mandatory.
The EXECUTE sp_executesql statement passes the datetime arguments to the dynamic query and then executes it.
One other note is about your use of the BETWEEN predicate with datetime values. If Transaction_date includes timestamps with non-zero time portions, it would be much better to specify the range in this form:
[Transaction_date] >= #AuditPeriodStartDate
and
[Transaction_date] < #AuditPeriodEndDate
That way you can be sure the results will include only relevant values. More information can be found in this blog article:
What do BETWEEN and the devil have in common?

SQL Variables as Column names in Where Clause [duplicate]

This question already has answers here:
Can I pass column name as input parameter in SQL stored Procedure
(9 answers)
Closed 4 years ago.
I need some help with my SQL logic, and I've been working (and researching) this for 2 days now with zero success.
My goal is to try an pass a variable from an ASP page to a stored procedure, which is utilizing the variable as criteria for a column name in the where clause.
So for example (a simplified version of my query):
#strDept nvarchar(10), #strUser nvarchar(30)
-- The asp page will pass f18 to #strDept & Ted Lee to strUser
-- f18 is the column name in my database that I need in the where.
select x, y, z from table1 where #strDept in (#strUser)
-- and this is the select statement, notice the where clause.
The stored procedure does execute, but it returns no values and I know its treating the #strDept as a literal nvarchar and not a column name.
So I guess my question is, how do I get SQL Server 2005 to treat my #sqlDept variable as a column name?
The reason you can't find guidance on how to do this is that it's a really bad idea.
Sooner or later, someone is going to pass a "column name" of 1 ;drop database badidea. Which will be a blessing for all concerned.
Read up on SQL Injection, and rethink your design.
If this is an internal company application why is everyone re-iterating and beating SQL Injection to death... Its very simple to just use Dynamic SQL.
If you are comfortable that these are only internal users using this then its very simple. Here is the concept. You essentially write a SQL Statement that writes a string that is really a SQL statement and then execute it.
CREATE Procedure myDynamicProcedure
#strDept nvarchar(10),
#strUser nvarchar(30)
as
BEGIN
1. Declare a variable to store the SQL Statement.
DECLARE #SQL varchar(max)
2. SET your #SQL Variable to be the SELECT Statement. Basically you are building it so it returns what you are wanting to write. Like this:
SET #SQL = 'select x, y, z from table1 where' + #strDept +
' in ' + #strUser
3. Execute the #SQL Statement and it will be exactly like you ran:
SELECT x,y,z from table1 where f18 = 'Ted Lee'
EXEC (#SQL)
END
Why do you want to make column name dynamic? What do you plan to achieve? You can use dynamic query like answer above but injection attacks may start.
If you explain what you want to do with that maybe we can recommend another solution.
You can use some dynamic sql e.g.
DECLARE #sqlDept VARCHAR(100)='CURRENT_TIMESTAMP';
EXEC('SELECT '+#sqlDept)
In your case this will be
DECLARE #strDept nvarchar(10)='dept1'
,#strUser nvarchar(30)='user1';
DECLARE #DynamicSql nvarchar(1000);
SET #DynamicSql='select x, y, z from table where '+#strDept+' in ('''+#strUser+''')';
Then
SELECT #DynamicSql;
Will give you:
select x, y, z from table where dept1 in ('user1')
To execute this statement you do this as
EXEC(#DynamicSql);
Another alternative is to use a small bit of substitution in the proc. This still uses dynamic SQL, but you are never executing user supplied values.
DECLARE #userSuppliedValue VARCHAR(50) = 'JOHNNY DROP TABLES'
DECLARE #substValue VARCHAR(50)
IF #userSuppliedValue = 'Table1'
SET #substValue = 'Table1'
IF #userSuppliedValue = 'Table2'
SET #substValue = 'Table2'
/*Repeat for N permutations*/
/* Throw an error if you think its necessary to do so when no match is found*/
IF #substValue IS NULL
RAISERROR(1,1,'errah')
EXEC ('SELECT * FROM ' + #substValue)
I think the best way is to build a dynamic SQL and add a lookup to see if the column exist and prevent SQL injection in the column name.
declare #strDept nvarchar(10), #strUser nvarchar(30),
#sql nvarchar(300), #found smallint
set #strDept = 'f18'
set #strUser = 'Ted Lee'
set #found = (SELECT count(*)
FROM syscolumns
WHERE id=OBJECT_ID('table1') AND name=''+#strDept+'')
set #sql = 'select x, y, z from table1 where ' + #strDept + ' in ('''+#strUser+''')'
if #found = 1 exec (#sql)
SQL injection testing : See SQL FIDDLE : http://www.sqlfiddle.com/#!6/df3f6/18/0
DECLARE #value varchar(10)
SET #value = 'intStep'
DECLARE #sqlText nvarchar(1000);
SET #sqlText = N'SELECT ' + #value + ' FROM dbo.tblBatchDetail'
Exec (#sqlText)

Simple dynamic TSQL query syntax

This may be an easy answer but I've been staring at it for too long...
I have the following query that takes a stored procedure input parameter as a variable name and counts the records in that table. I'd like to retrieve the results of the dynamic statement (#toStartStr) into a variable (#toStart).
-- #tempTableName = SProc input parameter
DECLARE #toStartStr nvarchar(150);
DECLARE #toStart int;
SET #toStartStr = 'SELECT #toStart = COUNT(ID) FROM ' + #tempTableName;
EXEC(#toStartStr);
Right now, an error suggests that #toStart cannot be concatenated with the string SELECT, but this is the gist of what I want. Can anyone see what I'm doing wrong? Or suggest an alternative? FYI SQL 2008 R2. Thanks.
DECLARE #sql NVARCHAR(255);
DECLARE #toStart INT;
SET #sql = N'SELECT #toStart = COUNT(ID) FROM ' + QUOTENAME(#tempTableName);
EXEC sp_executesql #sql, N'#toStart INT OUTPUT', #toStart OUTPUT;
PRINT #toStart;
However there is a much easier and more efficient way to do this, if you're okay with ignoring current in-flight transactions (and you're using SQL Server 2005 or better - please specify the version when asking questions!).
DECLARE #toStart INT;
SELECT #toStart = SUM(rows)
FROM sys.partitions
WHERE [object_id] = OBJECT_ID(#tempTableName)
AND index_id IN (0,1);
PRINT #toStart;
Just for completeness, here is a solution for SQL Server 2000, which also doesn't require any special privileges (just connect and member of public):
DECLARE #toStart INT;
SELECT #toStart = [rows]
FROM sysindexes
WHERE id = OBJECT_ID(#tempTableName)
AND indid IN (0,1);
PRINT #toStart;
That said, if you're using a count to determine what the next ID might be, or something like that, I think you're approaching this the wrong way, since rows can be deleted and if it's an identity column values can be skipped due to rollbacks.

How can I spot in what database is a stored procedure with name 'myStoredProcedure'?

There are bunch of databases to the SQL server I am connected.
How should I query the sysobjects in order to spot in what database a stored procedure with name 'myStoredProcedure' is located ?
The query should return the database name.
Thanks
I know you are not asking for this, but I'd really download RedGate's Sql Search add-in for SSMS and use that. It allows you to find any object (proc, table, view, column, etc) on any database easily.
And it's free!
I'd give this a try:
CREATE TABLE ##DatabaseList
(
DatabaseName varchar(50)
)
EXECUTE SP_MSForEachDB 'USE [?]; INSERT INTO ##DatabaseList SELECT DB_NAME() FROM [sys].[objects] WHERE name = "MyStoredProcedure" AND type_desc = "SQL_STORED_PROCEDURE"'
SELECT * FROM ##DatabaseList
DROP TABLE ##DatabaseList
That's using the undocumented/ unsupported system stored procedure SP_MSForEachDb and writing any hits to a global temp table, then outputting the contents to the Results window before dropping the table. If you just need to know which database (or databases - there may of course be more than one) has an appropriately named SP, this should do it. If you want to use the output elsewhere as a parameter, it may take a little more work.
By the way, I'm only learning this stuff myself over the last few months so if anyone can critique the above and suggest a better way to go at it I'm happy to receive feedback. Equally, I can answer any further questions posted here to the best of my ability.
Cheers
So out of curiosity I decided to try write this myself, especially since ADG mentioned his solution was using an unsupported, undocumented procedure. This could also be expanded to take a 2nd parameter so where it checks the type = P (stored Proc) you could probably change it to look for other things like views / tables etc.
My solution is a bit long but here goes:
CREATE PROCEDURE spFindProceduresInDatabases
(
#ProcedureName NVARCHAR(99)
)
AS
BEGIN
-- Get all the database names and put them into a table
DECLARE #Db TABLE (DatabaseName Varchar(99))
INSERT INTO #Db SELECT name FROM Sys.databases
-- Declare a table to hold our results
DECLARE #results TABLE (DatabaseName VARCHAR(99))
-- Make a Loop
-- Declare a variable to be incremented
DECLARE #count INT
SET #count = 0
-- Declare the end condition
DECLARE #endCount INT
SELECT #endCount = COUNT(*) FROM #Db
-- Loop through the databases
WHILE (#count < #endCount )
BEGIN
-- Get the database we are going to look into
DECLARE #dbWeAreChecking VARCHAR(99)
SELECT TOP 1 #dbWeAreChecking = DatabaseName FROM #Db
DELETE FROM #Db WHERE DatabaseName = #dbWeAreChecking
-- Create and execute our query
DECLARE #Query NVARCHAR(3000)
SET #Query = N'SELECT #outParam = COUNT(*) FROM '+#dbWeAreChecking+'.sys.sysobjects WHERE type = ''P'' and name = #ProcedureName'
Declare #outParam INT
print (#Query)
DECLARE #ParmDefinition NVARCHAR(500)
DECLARE #IntVariable INT
SET #ParmDefinition = N'#ProcedureName VARCHAR(99),#outParam INT OUTPUT'
SET #IntVariable = 35
EXECUTE sp_executesql
#Query ,
#ParmDefinition,
#ProcedureName,
#outParam = #outParam OUTPUT
-- If we have a result insert it into the results table
If (#outParam > 0)
BEGIN
INSERT INTO #results(DatabaseName) VALUES(#dbWeAreChecking)
END
-- Increment the counter
SET #count = (#count + 1)
END
-- SELECT ALL OF THE THINGS!!!
SELECT * FROM #results
END