For the sake of argument, let's just say I have to create a local variable containing a SQL query that has an INSERT:
DECLARE #insert NVARCHAR(MAX)
SELECT #insert = 'INSERT INTO [dbo].[' + #table + '] VALUES...
EXEC (#insert)
This INSERT is also going to contain a column value:
DECLARE #insert NVARCHAR(MAX)
SELECT #insert =
'INSERT INTO [dbo].[' + #table + '] VALUES (N''' + #message + ''')'
EXEC (#insert)
Now, I'm obviously concerned about an injection attack, and would like to ensure that #message's value can't make #insert's value malicious or malformed as a query to EXEC.
This brings us to my question: is escaping the ' characters in #message sufficient? Are there any other characters that could appear in #message that could escape out?
Example:
DECLARE #insert NVARCHAR(MAX)
SELECT #message = REPLACE(#message,'''','''''')
SELECT #insert =
'INSERT INTO [dbo].[' + #table + '] VALUES (N''' + #message + ''')'
EXEC (#insert)
(When I say "have to", this is because my query is in a stored procedure, and this stored procedure accepts #table, which is the destination table to INSERT into. I'm not interested in discussing my architecture or why the table to INSERT into is "dynamically" specified via a procedure parameter. Please refrain from commenting on this unless there's another way besides EXEC()ing a query to specify a table to INSERT into when then table name is received as a procedure parameter.)
Use sp_executesql and the built-in quotename(). This article, The Curse and Blessings of Dynamic SQL, is pretty much the definitive reference.
You could first query the schema information with regular T-SQL and make sure the table name exists first. This way, if it's malformed SQL, it won't execute as code. It will just be a VARCHAR table name.
DECLARE #Table AS VARCHAR(MAX)
DECLARE #Exists AS BIT
SET #Table = 'Vicious malformed dynamic SQL'
SELECT #Exists = COUNT(TABLE_NAME)
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = #Table
IF (#Exists = 1)
BEGIN
PRINT 'Table exists'
-- Execute dynamic SQL.
END
ELSE
PRINT 'Invalid table'
(Or simply use IF EXISTS (SELECT ....) )
Rather than calling EXEC(#somesql), I suggest using the sp_executesql stored procedure. Specifically, this allows you to pass parameters, and the system will check that the parameters are valid.
Apparently there's a 128-length limit to quotename(), even in 2008 according to my test, since it expects a SQL identifier. The reference suggests creating a quotestring() function, which does the same thing as:
REPLACE(#variable,'''','''''')
Therefore I am proposing that the answer is to create a function out of the REPLACE() above, like so:
CREATE FUNCTION quotestring(#string nvarchar(MAX))
RETURNS nvarchar(MAX) AS
BEGIN
RETURN(REPLACE(#string,'''',''''''))
END
...Unless I've misunderstood something.
When writing dynamic SQL you'll want to parameterise as much as possible, and only resort to character escaping when you absolutely have to. You can't parameterise #table, but you can parameterise #message.
DECLARE #insert NVARCHAR(MAX)
set #insert = 'INSERT INTO [dbo].' + quotename(#table) + ' values(#message)'
exec sys.sp_executesql #insert, N'#message nvarchar(max)', #message = #inMessage;
There are a lot of ways attackers can exploit dynamic SQL, including buffer length attacks and using unicode equivalent characters. I encountered an example once where escaping the single quote char still had a vulnerability where one of the unicode equivalents of the quote char could be passed in. Part of the software stack was doing a unicode to ascii conversion, so it was possible to inject a quote back in after they were escaped. Ouch.
Related
I am a beginner in SQL and esspecially dynamic sql execution.
I am trying to convert some dynamic sql which is inside a store procedure from using EXEC to using sp_executesql.
The issue I have is I don't know what are the exact rules for representing multiple lines of dynamic sql. The existing code is in the form of:
Set #cmd = 'WITH vdate AS' + char(13)
+' (SELECT valueID,' + char(13)
+'username)' +char(13)
+'FROM dbo.table1' + char(13)
+'WHERE username = ''' + convert(varchar(11), #username, 106) + ''' AND' + char(13)
Exec (#cmd)
The above is just a snippet of what it looks like. There are alot more lines and more complicated stuff going on.
I now want to be able to use the sp_executesql way of executing this code because I want to change the username attribute to be a table which accepts multiple usernames.
Please can you advise me what the syntax is for this and how do i make the line before last work?
I have seen that code looks very similar so what i did I changed the +' to be N' at the start of each line and the code compiled but it didn't execute when I tried to use the query in my application.
Thanks, Jetnor.
The only difference is that instead of EXEC (#cmd) you would use EXEC sp_executesql #cmd. Technically speaking, sp_executesql takes NVARCHAR arguments. In order to create an NVARCHAR string you should prefix the string with N like so:
DECLARE #cmd NVARCHAR(1000);
SET #cmd = N'My string'
This denotes that the string is a Unicode string which may contain national character sets. Of course you can omit the N but if you have anything other than ASCII characters in it then it won't work properly and you'll end up with strings that contain a lot of question marks instead of your special characters (CJK - Chinese, Japanese, Korean - character sets for instance) so it is good practice to always prefix NVARCHAR strings with the N.
Some of your confusion arises because you are replacing the string concatenation operator, +, with the national character set identifier, N. You will need both. It is always worth including proper spacing in your code, especially in dynamic SQL because it's easier to read. My preference is to add the spaces at the end of lines where possible, but that's just my personal preference. Also, drop the CHAR(13) stuff unless you really want to print it out in a certain way. Your example would become:
DECLARE #cmd NVARCHAR(1000) --Note this is NVARCHAR, not VARCHAR.
SET #cmd = N'WITH vdate AS '
+ N'(SELECT valueID, '
+ N'username) '
+ N'FROM dbo.table1 '
+ N'WHERE username = ''' + convert(NVARCHAR(11), #username, 106) + ''' AND ' --Note that you are missing a chunk here - AND what?
EXEC sp_executesql #cmd
Note that your CONVERT is incorrect: Firstly you should convert to NVARCHAR (if #username isn't already), and secondly the 106 part does nothing - that's for date formatting. Also you are missing the end of the statement as there is an AND and then nothing. It's always good to use PRINT #cmd to see if the SQL you've generated is valid (it will output the contents of #cmd and you can then copy that and paste it into a workshet in SSMS).
Now, you might want to have output parameters in your dynamic SQL as well which is no problem.
DECLARE #ID INT;
DECLARE #outputValue NVARCHAR(50);
DECLARE #cmd NVARCHAR(1000);
SET #cmd = N'SELECT #value = Value FROM MyTable WHERE ID = ' +
CAST(#ID AS NVARCHAR(5));
EXEC sp_executesql #cmd, '#value NVARCHAR(50) OUT', #outputValue OUT;
SELECT #outputValue;
Traditionally, the main reason for wanting to use sp_executesql as opposed to just EXEC is that you have a better chance of the execution plan being cached and therefore re-used later. I confess I'm not sure if that's still true or not, but sp_executesql is generally the preferred method of executing dynamic SQL. A lot of poeple have an instinctive hatred of dynamic SQL but frankly, like all things, it has it's uses so long as it isn't abused and used where it just isn't required.
Suppose I have many tables in my database. Every time I will insert data in any table. So, can I use a single stored procedure for all my data insertion in every table, instead of writing one stored procedure for each table?
In this scenario, each time I will pass table name and parameters dynamically to the stored procedure. If yes, can anyone give some basic idea how to perform this? If any extra information is required, please ask.
Thanks and regards,
Rizwan Gazi.
You could work with dynamic SQL and build the insert statement on the fly. THIS IS NOT RECOMMENDED but it should solve the problem you're asking about.
(I haven't run this code, but you can see what is being accomplished here with building the insert string and then executing it)
In this procedure, you pass in the table name, columns and values you care about and fire it off in a row based operation. With some minor tweaks you would be able to make it set based as well.
create procedure dbo.TableInsert
#tablename varchar(100)
, #columnlist varchar(max)
, #valueslist varchar(max)
as
begin
declare #sql varchar(max)
set #sql =
'insert into ' + #tablename
+ '(' + #columnlist + ')'
+ ' VALUES (' + #valueslist + ')'
print(#sql)
sp_executesql (#sql)
end
go
Execution would look something like this:
exec dbo.TableInsert
#tablename = 'TestTable'
, #columnlist = 'col1, col2, col3'
, #valuelist = '1,2,3'
Text insert would be a little trickier in this version since you have to wrap it around in single quotes.
exec dbo.TableInsert
#tablename = 'TestTable'
, #columnlist = 'col1, col2, col3'
, #valuelist = '''1'',''2'',''3'''
You could do something using dynamic SQL to build a query and then run it using:
EXEC SP_EXECUTESQL(#SQL)
(Assuming MS SQL Server)
Not sure I'd recommend it though and will probably be a total nightmare to test and maintain. Having different sprocs would be easier to test and maintain going forward and would perform better as the different sprocs would have separate query plans.
If you are working in code you could use a ORM to deal with basic CRUD stuff.
I am trying to store the results of an SQL query into a variable.The query simply detects the datatype of a column, hence the returned result is a single varchar.
SET #SQL =
'declare ##x varchar(max) SET ##x = (select DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS
WHERE Table_name = ' +char(39)+#TabName+char(39) +
' AND column_name = ' +char(39)+#colName+char(39) + ')'
EXECUTE (#SQL)
Anything within the 'SET declaration' cannot access any variables outside of it and vice versa, so I am stuck on how to store the results of this query in a varchar variable to be accessed by other parts of the stored procedure.
You dont need a dynamic query to achieve what you want, below query will give the same result as yours.
declare #x varchar(max)
declare #tableName varchar(100), #ColumnName varchar(50)
set #tableName = 'Employee'
set #ColumnName = 'ID'
select #x = DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS
where
Table_Name = #tableName
and column_name = #ColumnName
select #x
All user-defined variables in T-SQL have private local-scope only. They cannot be seen by any other execution context, not even nested ones (unlike #temp tables, which can be seen by nested scopes). Using "##" to try to trick it into making a global-variable doesn't work.
If you want to execute dynamic SQL and return information there are several ways to do it:
Use sp_ExecuteSQL and make one of the parameters an OUTPUT parameter (recommended for single values).
Make a #Temp table before calling the dynamic SQL and then have the Dynamic SQL write to the same #Temp table (recommended for multiple values/rows).
Use the INSERT..EXEC statement to execute your dynamic SQL which returns its information as the output of a SELECT statement. If the INSERT table has the same format as the dynamic SQL's SELECT output, then the data output will be inserted into your table.
If you want to return only an integer value, you can do this through the RETURN statement in dynamic SQL, and receive it via #val = EXEC('...').
Use the Session context-info buffer (not recommended).
However, as others have pointed out, you shouldn't actually need dynamic SQL for what you are showing us here. You can do just this with:
SET #x = ( SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS
WHERE Table_name = #TabName
AND column_name = #colName )
You may want to consider using the sp_executesql stored procedure for dynamic sql.
The following link provides a good usage example of sp_executesql procedure with output parameters:
http://support.microsoft.com/kb/262499
I have a Microsoft SQL stored procedure whose column name I want to set via a variable that is passed into it:
CREATE PROCEDURE [My_Procedure]
#myDynamicColumn varchar(50)
AS BEGIN
SELECT 'value' AS #myDynamicColumn
END
This does not work ("Incorrect syntax"). If I wrap the column name with [ ]:
SELECT 'value' AS [#myDynamicColumn]
The column name literally outputs as '#myDynamicColumn' instead of the actual value. Is there any way to do this? I've looked into dynamic SQL articles but nothing is quite what I'm asking for.
EXEC ('SELECT ''value'' AS ' + #myDynamicColumn)
You could build your query into a string and use exec
CREATE PROCEDURE [My_Procedure]
#myDynamicColumn varchar(50)
AS BEGIN
EXEC('SELECT ''value'' AS ' + #myDynamicColumn)
END
Both the upvoted answers are very dangerous here, both are wide open to injection attacks and should not be used.
When injecting dynamic object names you must ensure you properly quote your object names. SQL Server has a built in function for that, QUOTENAME. Thus what you should actually be doing is the following:
CREATE PROCEDURE [My_Procedure] #myDynamicColumn sysname
AS BEGIN
DECLARE #SQL nvarchar(MAX) = N'SELECT ''value'' AS ' + QUOTENAME(#myDynamicColumn) + N';';
EXEC sys.sp_executesql #SQL;
END
You'll note I also change the data type of the parameter to sysname, a synonym for nvarchar(128) NOT NULL, which is the data type SQL Server uses internally for object names.
[Update: Using SQL Server 2005]
Hi, what I want to do is query my stored procedure with a comma-delimited list of values (ids) to retrieve rows of data.
The problem I am receiving is a conversion error:
Conversion failed when converting the varchar value ' +
#PassedInIDs + ' to data type int.
The statement in my where-clause and error is:
...
AND (database.ID IN (' + #PassedInIDs + '))
Note: database.ID is of int type.
I was following the article at:
http://www.sql-server-helper.com/functions/comma-delimited-to-table.aspx
but did not complete because of the error.
In my execution script I have:
...
#PassedInIDs= '1,5'
Am I doing something wrong here?
Thank you for your help.
I would strongly suggest that you use the second method from that link. Create a user-defined function that turns your comma-delimited string into a table, which you can then select from easily.
If you do a Google on Erland and "Dynamic SQL" he has a good writeup of the pitfalls that it entails.
For one, you are passing a string to the IN function in SQL. If you look back at the original article, you'll see that instead of issuing a direct SQL statement, it instead is building a string which is the SQL statement to execute.
There is no string evaluation in SQL. This:
database.ID IN (' + #PassedInIDs + ')
will not be turned to:
database.ID IN (1,2,3)
just because the #PassedInIDs parameter happens to contain '1,2,3'. The parameter is not even looked at, because all you have is a string containing " + #PassedInIDs + ". Syntactically, this is equivalent to:
database.ID IN ('Bob')
To make it short, you can't do what you attempt here in SQL. But there are four other possibilities:
you construct the SQL string in the calling language and abandon the stored procedure altogether
you use a dynamic prepared statement with as many parameters in the IN clause as you pan to use
you use a fixed prepared statement with, say, 10 parameters: IN (?,?,?,?,?,?,?,?,?,?), filling only as many as you need, setting the others to NULL
you create a stored procedure with, say, 10 parameters and pass in as many as you need, setting the others to NULL: IN (#p1, #p2, ..., #p10).
I would create a CLR table-valued function:
http://msdn.microsoft.com/en-us/library/ms131103.aspx
In it, you would parse the string apart and perform a conversion to a set of rows. You can then join on the results of that table, or use IN to see if an id is in the list.
You need to treat ufn_CSVToTable like it's a table. So you can join the function:
JOIN ufn_CSVToTable(#PassedInIDs) uf ON database.ID = uf.[String]
I suggest using XML for this in SQL 2005. Somewhat bulkier, but it can be easier. It allows you to select the XML into a table which can then be joined or inserted etc.
Look at Sql Server's OPENXML() if you haven't already.
For example, you could pass in something like:
'12...'
and then use:
exec sp_xml_preparedocument #doc OUTPUT, #xmlParam
SELECT element
FROM OPENXML (#doc, 'Array/Value', 2) WITH (element varchar(max) 'text()')
That should be a start
this may be solved by 6 ways as mentioned in Narayana's article Passing a list/array to an SQL Server stored procedure
And my most strait forward implementation is
declare #statement nvarchar(256)
set #statement = 'select * from Persons where Persons.id in ('+ #PassedInIDs +')'
exec sp_executesql #statement
-
Here is what I have found and tested:
SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
GO
CREATE FUNCTION [dbo].[SplitStrings] ( #IDsList VARCHAR(MAX) )
RETURNS #IDsTable TABLE ( [ID] VARCHAR(MAX) )
AS
BEGIN
DECLARE #ID VARCHAR(MAX)
DECLARE #Pos VARCHAR(MAX)
SET #IDsList = LTRIM(RTRIM(#IDsList)) + ','
SET #Pos = CHARINDEX(',', #IDsList, 1)
IF REPLACE(#IDsList, ',', '') <> ''
BEGIN
WHILE #Pos > 0
BEGIN
SET #ID = LTRIM(RTRIM(LEFT(#IDsList, #Pos - 1)))
IF #ID <> ''
BEGIN
INSERT INTO #IDsTable
( [ID] )
VALUES ( CAST(#ID AS VARCHAR) )
END
SET #IDsList = RIGHT(#IDsList, LEN(#IDsList) - #Pos)
SET #Pos = CHARINDEX(',', #IDsList, 1)
END
END
RETURN
END
GO
Here is how function Call:
SELECT * FROM dbo.SplitStrings('123,548,198,547,965')
Try this:
DECLARE #Ids varchar(50);
SET #Ids = '1,2,3,5,4,6,7,98,234';
SELECT *
FROM sometable
WHERE ','+#Ids+',' LIKE '%,'+CONVERT(VARCHAR(50),tableid)+',%';