Escape strings when creating a dynamic update statement - string

I have a stored procedure that is generating a string that it will call EXECUTE() on. The string contains an UPDATE statement. However, the columns and values that it is executing are not known beforehand. These are coming into the stored procedure through an XML string. I then use XML queries to get the data out and into a temporary table.
This is not sanitizing the data.
DECLARE #TBL_FLD TABLE (
TBL VARCHAR(MAX),
COL VARCHAR(MAX),
VAL VARCHAR(MAX)
);
-- Fill #TBL_FLD via xml parsing (omitted for brevity)
DECLARE TBL_CURSOR CURSOR FOR
SELECT distinct (TBL) FROM #TBL_FLD;
OPEN TBL_CURSOR;
WHILE 1 = 1
BEGIN
FETCH NEXT FROM TBL_CURSOR INTO #TABLE_NAME
IF ( ##FETCH_STATUS <> 0 )
BREAK
SET #SETTING_STR = '';
SELECT #SETTING_STR = STUFF( ( SELECT ', ' + COL + ' = ''' + VAL + '''' FROM #TBL_FLD WHERE TBL = #TABLE_NAME FOR XML PATH('') ), 1, 2, '');
SET #SQL_QUERY += 'UPDATE ' + #TABLE_NAME + ' SET ' + #SETTING_STR + ' WHERE KEY = ' + CONVERT(VARCHAR(MAX), #KEY_VAL) + '; ';
END
CLOSE TBL_CURSOR
DEALLOCATE TBL_CURSOR
EXECUTE (#SQL_QUERY);
I trust the COL field of #TBL_FLD, but the VAL will have come from a user. Which leaves a massive security hole since I am just concatenating the data together. There has to be a better way.
Since there are an unknown number of columns, I can't easily create parameters for the statement so that the data is cleaned up. If worst comes to worst, I can do it, (see the answer to Dynamically Create Update SQL In Stored Procedure) but it will be uglier than I would like.
Is there a function, or method, to sanitize the data, before I blindly add it to the statement? Or is there a better way to do what I'm trying to do?

I think that just doubling single quotes with a REPLACE on VAL would fix any issues since an attacker would not be able to exit the "string scope" to execute arbitrary code :
SELECT #SETTING_STR = STUFF( ( SELECT ', ' + COL + ' = ''' + REPLACE(VAL, '''', '''''') + '''' FROM #TBL_FLD WHERE TBL = #TABLE_NAME FOR XML PATH('') ), 1, 2, '');
I don't remember a built-in T-SQL function that does the same thing

Related

SELECT SCOPE_IDENTITY() to return the last inserted ID

There are a few answers about this problem, but my question is about the particular code I have.
I'm trying to get the last inserted ID of this query executing on VBA code.
Public Function Execute(cQry As excfw_dbQuery) As ADODB.Recordset
If pConn.State = 0 Then
OpenConnection
End If
qry = "INSERT INTO [some really long query, which actually works]; SELECT SCOPE_IDENTITY()"
On Error Resume Next
Dim rs As ADODB.Recordset
Set rs = New ADODB.Recordset
rs.Open qry, pConn 'also tried with adOpenKeyset, adLockOptimistic
'some error handling code which is not related to the issue
Set rs = rs.NextRecordset() 'also tried without moving onto the next recordset
pInsertedId = rs.Fields(0).Value
Set Execute = rs 'this is just to pass the query result for SELECT queries
End Function
This should save the last inserted ID on the pInsertedId variable, but instead I get 0 each time I insert a row. The weird thing is, when I copy and paste the same code into the SSMS, it works.
I might just get away with inserting some unique data to some unused column of the database and querying through that.
--UPDATE--
I've just noticed that when running a SELECT query, rs object remains open until it goes out of scope. Here is a screenshot of the watch section:
on an insert statement instead, it gets closed as soon as the query gets executed:
You can explicitly save the results of the insert statement by using an output clause and return the results with a select:
qry =
"declare #Ids as ( Id Int );" +
"insert into MyTable ( Name ) " + ' Assuming Id is an identity column.
"output Inserted.Id into #Ids " +
"values ( #Name );" +
"select Id from #Ids;"
From the documentation for output:
INSERTED Is a column prefix that specifies the value added by the
insert or update operation. Columns prefixed with INSERTED reflect the
value after the UPDATE, INSERT, or MERGE statement is completed but
before triggers are executed.
You can use an output clause to get any data from the rows (Note plural.), e.g. identity column values for newly inserted rows. Output can be used with insert, update, delete and merge and provides access to both before and after values in the case of update. A tool well worth having in your pocket.
As it turns out, the table that I'm trying to insert to has multiple triggers attached to it. So, the query which includes the SELECT SCOPE_IDENTITY(); is actually 4th query. I had to move 4 queries forward in order to get the correct scope. How I pulled that off programatically is as follows. Not very clean (and possibly not the best way to do it), but does the job for me.
I basically go ahead to next recordset until there is none left, which I detect by checking the error number 91 (Object variable or with block variable not set)
Public Function Execute(cQry As excfw_dbQuery) As ADODB.Recordset
If pConn.State = 0 Then
OpenConnection
End If
qry = "INSERT INTO [some really long query, which actually works]; SELECT SCOPE_IDENTITY()"
On Error Resume Next
Dim rs As ADODB.Recordset
Set rs = New ADODB.Recordset
rs.Open cQry.Query, pConn, adOpenKeyset, adLockOptimistic
'some error handling code which is not related to the issue
On Error Resume Next
'begin loop
Do
'go to next recordset
Set rs = rs.NextRecordset()
'if we're getting error n. 91, it means
'recordsets are exhausted, hence we're getting
'out of the loop
If Err.Number = 91 Then
Err.Clear
Exit Do
End If
'if we are not out of recordsets, check the
'result of the query. If it is bigger then zero,
'it means we hit the jackpot.
If rs.Fields(0).Value > 0 Then
pInsertedId = rs.Fields(0).Value
End If
Loop
On Error GoTo 0
End Function
Again, not the cleanest, nor the most correct way to do it, but did the trick. I'm open for any improvements or suggestions.

Replace SQL query parameter in a Excel power query

I have a workbook where I fetch data from SQL Server using fixed parameter values for a SQL query.
I want to make another sheet and have the parameter for the SQL query be taken from the cell values.
I didn't find anything on this regard.
Also I would like to refresh the data as soon as the cell values changes in the other sheet.
For this to work, you need to set up three different parts:
1) A parameter table in an Excel sheet
2) Changes to the advanced editor in PowerQuery
3) A macro to refresh the PQ when any cells in the parameter table are changed
1) Excel Table
You can see I included a column called param which can hold a Parameter name to help keep straight which parameter is which.
2) PQ Advanced Editor
let
ParamTable = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
Param = "'" & Text.From(ParamTable[value]{0}) & "'",
Source = Sql.Database("IP Address", "Database Name", [Query="Select * from weeks#(lf)where date >= '2018-01-01' and date < " &Param])
in
Source
Equivalent alternative: (Difference in location of variable used in SQL query.)
let
ParamTable = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
Param = "'" & Text.From(ParamTable[value]{0}) & "'",
Source = Sql.Database("IP Address", "Database Name", [Query="Select * from weeks#(lf)where date < " &Param & " and date >= '2018-01-01'"])
in
Source
Alternative Variable Type: (If dealing with numbers, the string markers ' aren't required)
let
ParamTable = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
Param = Text.From(ParamTable[value]{0}),
Source = Sql.Database("IP Address", "Database Name", [Query="Select * from weeks#(lf)where cnt < " &Param & " and date >= '2018-01-01'"])
in
Source
Explanation:
After pulling the Parameter Table into the PQ Query (ParamTable = Excel.CurrentWorkbook(){[Name="Table1"]}[Content]), the columns can be accessed by column name [value] and the rows by a zero-index number {0}. Since I was pulling in a date-value. I needed to convert it to a string value I could insert into the SQL Query -- thus the Text.From() and the appended ''s to the ends (SQL marks strings with single ' rather than the double ")
Since I named the variable Param, to use it in the string, I substituted &Param for the value which had originally been there.
2.1 Power Query's Value.NativeQuery
let
ParamTable = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
Param = ParamTable[value]{0},
Source = Value.NativeQuery(Sql.Database("IP Address", "Database Name"), "Select * from weeks where date < #dateSel and date >= '2018-01-01'",[dateSel = Param])
in
Source
Alternative Formatting:
let
ParamTable = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
Param = ParamTable[value]{0},
Source = Sql.Database("IP Address", "Database Name"),
Data = Value.NativeQuery(Source, "
Select * from weeks
where date < #dateSel and date >= '2018-01-01'
",[dateSel = Param])
in
Source
Notes:
When using Value.NativeQuery(), you can pass a date or datetime value directly in as a variable without having to include the single apostrophes.
Sometimes splitting the data retrieval into a Source step and a NativeQuery step can help with PQ's sporadic firewall issues.
3) Macro
This works for a simple check if anything in the table has changed, then runs the refresh. You will need to make sure this is placed in the correct module. Items you will need to change are:
Sheet1 is the codename of the worksheet with the parameter table.
"Table" is the name of the Parameter table
"Query - Query1" is the name of the connection to be refreshed
Note: Query1 is the name of the query. Default names for connections are usually Query - & query name
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Sheet1.ListObjects("Table1").DataBodyRange) Is Nothing Then
ThisWorkbook.Connections("Query - Query1").Refresh
End If
End Sub

SSRS: How can I use a Search parameter with multiple values?

I am creating a report that the business would like to use to spot check 5, 10 or 20 records at a time.
The request was to use a Search parameter (as opposed to dropdown params).
How can I create a Search parameter that will allow for multiple values separated by a comma?
Thanks for the help.
Don't worry about the length of this answer, most of it is a cut/paste job ! I've tried to explain each bit as we go so you understand it better, the actual amount of code you need to craft is minimal.
If you have SQL Server 2016 then you can take advantage of the new string_split function, if you have an older version you'll have to create a similar function yourself, or copy the one I created a few years back which does a similar thing.
Lets get the function created first: I've created it in the fn schema but you can obviously change this to whatever schema you like.
CREATE FUNCTION [fn].[Split](#sText varchar(8000), #sDelim varchar(20) = ' ')
RETURNS #retArray TABLE (idx smallint Primary Key, value varchar(8000))
AS
BEGIN
DECLARE #idx smallint,
#value varchar(8000),
#bcontinue bit,
#iStrike smallint,
#iDelimlength tinyint
IF #sDelim = 'Space'
BEGIN
SET #sDelim = ' '
END
SET #idx = 0
SET #sText = LTrim(RTrim(#sText))
SET #iDelimlength = DATALENGTH(#sDelim)
SET #bcontinue = 1
IF NOT ((#iDelimlength = 0) or (#sDelim = 'Empty'))
BEGIN
WHILE #bcontinue = 1
BEGIN
--If you can find the delimiter in the text, retrieve the first element and
--insert it with its index into the return table.
IF CHARINDEX(#sDelim, #sText)>0
BEGIN
SET #value = SUBSTRING(#sText,1, CHARINDEX(#sDelim,#sText)-1)
BEGIN
INSERT #retArray (idx, value)
VALUES (#idx, #value)
END
--Trim the element and its delimiter from the front of the string.
--Increment the index and loop.
SET #iStrike = DATALENGTH(#value) + #iDelimlength
SET #idx = #idx + 1
SET #sText = LTrim(Right(#sText,DATALENGTH(#sText) - #iStrike))
END
ELSE
BEGIN
--If you can't find the delimiter in the text, #sText is the last value in
--#retArray.
SET #value = #sText
BEGIN
INSERT #retArray (idx, value)
VALUES (#idx, #value)
END
--Exit the WHILE loop.
SET #bcontinue = 0
END
END
END
ELSE
BEGIN
WHILE #bcontinue=1
BEGIN
--If the delimiter is an empty string, check for remaining text
--instead of a delimiter. Insert the first character into the
--retArray table. Trim the character from the front of the string.
--Increment the index and loop.
IF DATALENGTH(#sText)>1
BEGIN
SET #value = SUBSTRING(#sText,1,1)
BEGIN
INSERT #retArray (idx, value)
VALUES (#idx, #value)
END
SET #idx = #idx+1
SET #sText = SUBSTRING(#sText,2,DATALENGTH(#sText)-1)
END
ELSE
BEGIN
--One character remains.
--Insert the character, and exit the WHILE loop.
INSERT #retArray (idx, value)
VALUES (#idx, #sText)
SET #bcontinue = 0
END
END
END
RETURN
END
Once the function is created you can see what it outputs by doing something like
select * from fn.Split('Austria, Belgium, France', ',')
This returns the following
idx value
0 Austria
1 Belgium
2 France
Lets assume we have a geography table with the names of countries and their associated region, we can search for matching entries by simply joining to the output of the function something like this.
select g.CountryID, g.CountryDesc, g.ContinentDesc from dim.Geography g
join (SELECT * FROM fn.Split('Austria, Belgium, France', ',')) s
on g.CountryDesc = s.value
This, in my case, give me this output.
CountryID CountryDesc ContinentDesc
21 Austria West Europe
28 Belgium West Europe
89 France West Europe
To use the split function in your SSRS dataset, simply pass in the search text parameter so the query would now look something like this.
select g.CountryID, g.CountryDesc, g.ContinentDesc from dim.Geography g
join (SELECT * FROM fn.Split(#MySearchText, ',')) s
on g.CountryDesc = s.value

Using hash sign (#) in the Excel sheet name

I'm trying to solve an issue I have when I'm trying to use OLE DB for reading Excel files.
I found that the problem is because there is a hash mark (#) in the sheet name.
Unfortunately, I can't rename the sheet.
So after some tries, I've succeeded to read a full sheet by adding quotation marks ('):
Before
Select * from [" + sheetName + "$];
After (working)
Select * from ['" + sheetName + "$'];
But then I got stuck when trying to read a range from the sheet with the OLE DB feature:
Select * from [" + sheetName + "$" + fromCell + ":" + toCell + "];
When I try to send this command, it's seems like the # is replaced by . and then it cannot find the sheet.
I've tried many combination and escape codes and didn't find any solution. How can I access this file?
Your final output should look like this
'MySheet$A1:B2'
So your select should be
var SheetName = "MySheet";
var fromCell = "A1";
var toCell = "B2";
var sql = "Select * from ['" + SheetName + "$" + fromCell + ":" + toCell + "']";
Console.WriteLine(sql);
// Output
// Select * from ['MySheet$A1:B2']
Also consider parametrising your sql for better readability and also preventing sql code injection. You can find a guide for how to do it at OleDbCommand.Parameters.

SQL Server 2005: Two or more string into one string and pass as parameter [duplicate]

This question already has answers here:
Closed 11 years ago.
Possible Duplicate:
Parameterizing a SQL IN clause?
Hi All,
I am writing a SQL command for my project. I have to pass one string parameter to cursor.
But at that parameter I have to concatenate many strings, like this:
DECLARE #strParam VARCHAR(MAX)
SET #strParam = 'string1' + ',' + 'string2' + ',' ... etc
Then I want to use like this:
SELECT * FROM tblResult WHERE Field1 IN (#strParam)
instead of the following statement:
SELECT * FROM tblResult WHERE Field1 IN ('string1' + ',' + 'string2' + ',' ... etc)
So I need to get the format as like we set above.
How can I do that?
Best Regards,
Reds
This will split the csv into a table.
CREATE FUNCTION GetIDs(
#ids_csv nvarchar(255))
RETURNS #table_ids TABLE
(
ID INT,
PRIMARY KEY(ID)
) AS
BEGIN
DECLARE #app_id varchar(10)
DECLARE #pos int
SET #ids_csv = LTRIM(RTRIM(#ids_csv))+ ','
SET #pos = CHARINDEX(',', #ids_csv, 1)
IF REPLACE(#ids_csv, ',', '') <> ''
BEGIN
WHILE #pos > 0
BEGIN
SET #app_id = LTRIM(RTRIM(LEFT(#ids_csv, #pos - 1)))
INSERT INTO #table_ids(ID) VALUES(#app_id)
SET #ids_csv = RIGHT(#ids_csv, LEN(#ids_csv) - #pos)
SET #pos = CHARINDEX(',', #ids_csv, 1)
END
END
RETURN
END
Then, you can do this:
SELECT * FROM tblResult WHERE Field1 IN (SELECT * FROM GetIDs(#strParam))

Resources