I am currently working on an asp.net application that has sql server 2008 as its backend. I want to give the user the ability to specify what they want to filter by on the SQL statement.
On the interface I am giving them the option to select the following as a dropdown:
equals to
greater than
Less than
etc
I want to pass this as a parameter on the sql query to be executed. How best can I achieve this?
for eg;
Select amount, deduction, month from loan where amount #operant 10000;
the #operand is the return values of the above dropdown which is = < > <= >=
Assuming all positive integers < 2 billion, this solution avoids multiple queries and dynamic SQL. OPTION (RECOMPILE) helps thwart parameter sniffing, but this may not be necessary depending on the size of the table, your parameterization settings and your "optimize for ad hoc workload" setting.
WHERE [Amount] BETWEEN
CASE WHEN #operand LIKE '<%' THEN 0
WHEN #operand = '>' THEN #operant + 1
ELSE #operant END
AND
CASE WHEN #operand LIKE '>%' THEN 2147483647
WHEN #operand = '<' THEN #operant - 1
ELSE #operant END
OPTION (RECOMPILE);
I would write few "IF" statements. Code is not very short, but should be fast.
IF(#operand = '=')
Select..
ELSE IF(#operand = '>=')
Select..
...
Also, i would say, that Top (#someRowCount) could be great idea.
You need dynamic sql for this scenario
For your example this can be
DECLARE #sql AS nvarchar(max) -- Use max if you can, if you set
-- this to a specific size then your assignment later can be
-- truncated when maintained and still be valid.
SET #sql = 'Select amount, deduction, month from dbo.loan where amount '
+ #operand + ' 10000'
EXEC sp_executesql #sql
Update 1
There are 2 ways to execute dynamic sql : Exec() and sp_executesql
Read the comments why sp_executesql is preferred (still, beware of sql injections!)
I also prefix the table with the dbo so that the execution plan can be cached between different users
More info in the awesome paper at http://www.sommarskog.se/dynamic_sql.html#queryplans
Related
I would like to store security information for records in a SQL Server database. The security info would ideally be in the same form as what you might see in a config file, for consistency purposes:
<authorization>
<allow roles="Admins"/>
<allow users="SomeGuy,SomeOtherGuy"/>
<deny users="*"/>
</authorization>
I'd then like to be able to query the database for everything that a particular user is permitted access to, given their username and a list of their roles.
Does anyone have a suggestion on how best to do this? Or am I going about this the wrong way?
An easy brute force solution would be to just read every row in the database and pull each security rule XML into some class that will do the evaluation for me - but obviously that's going to be slow and on large tables will be unreasonable.
Another thing that comes to mind is making a child table of some kind which includes a priority of some kind to indicate the order in which each allow or deny node should be applied. However, I have quite a few tables that need this feature, and if I can avoid creating a ton of child tables, that would be ideal.
Though I have limited experience with XML columns in SQL Server, I can probably build an XML query to determine if a user is allowed - something starting with (/authorization/allow/#users)[1], perhaps. However, the order of the nodes matters, so while I could probably find a node that matches a given name or role, I don't know how to do any sort of set-based operation to check whether the user is denied or allowed based on which comes first.
So, given a user name and a comma delimited list of roles, what is the best way to check that person's access rights on a particular row in the database?
Well, i've come up with a solution, but it's not ideal. For 10,000 records, it takes 5 seconds to return all of the rows which match the security profile. This isn't a total disaster, and it does work, but i'll have to come back to this problem later to improve it.
Here's how i solved it. Keep in mind that i only worked on this for a few hours.
Before i could really do anything, i knew i was going to need a function to compare two comma delimited lists. I need to have a user's roles in a list, and see if any of those roles appear in the authorization settings stored in my xml column, as detailed in the original post. For this, i made two functions.
The first function is a commonly seen one to do string splitting using xml:
IF EXISTS (
SELECT * FROM sysobjects WHERE id = object_id(N'ufnSplitStrings')
AND xtype IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION ufnSplitStrings
GO
CREATE FUNCTION dbo.ufnSplitStrings
(
#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(#List, #Delimiter, '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
);
With that function established, i could then create another function which would then do the comparison i wanted:
IF EXISTS (
SELECT * FROM sysobjects WHERE id = object_id(N'ufnContainsAny')
AND xtype IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION ufnContainsAny
GO
CREATE FUNCTION dbo.ufnContainsAny(#List1 NVARCHAR(MAX), #List2 NVARCHAR(MAX))
RETURNS int
AS
BEGIN
DECLARE #Ret AS INT = 0
SELECT #Ret = COUNT(*) FROM dbo.ufnSplitStrings(#List1, ',') x
JOIN dbo.ufnSplitStrings(#List2, ',') y ON x.Item = y.Item
RETURN #Ret
END;
GO
Finally, i could use that function to assemble my main UserIsAuthorized function.
IF EXISTS (
SELECT * FROM sysobjects WHERE id = object_id(N'ufnUserIsAuthorized')
AND xtype IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION ufnUserIsAuthorized
GO
CREATE FUNCTION dbo.ufnUserIsAuthorized(#SecurityRules XML, #UserName NVARCHAR(64), #UserRoles NVARCHAR(MAX))
RETURNS int
AS
BEGIN
DECLARE #ret int = 0;
DECLARE #AuthType NVARCHAR(32);
DECLARE #authRules Table (a nvarchar(32), u nvarchar(max), r nvarchar(max), o int)
INSERT INTO #authRules
SELECT
a = value.value('local-name(.[1])', 'varchar(32)'),
u = ',' + value.value('#users', 'varchar(max)') + ',',
r = ',' + value.value('#roles', 'varchar(max)') + ',',
o = value.value('for $i in . return count(../*[. << $i]) + 1', 'int')
FROM #SecurityRules.nodes('//allow,//deny') AS T(value)
SELECT TOP 1 #AuthType = a FROM #authRules
WHERE CHARINDEX(',' + #UserName + ',', u) > 0 OR CHARINDEX(',*,', u) > 0 OR dbo.ufnContainsAny(r, #UserRoles) > 0 OR CHARINDEX(',*,', r) > 0
GROUP BY a
ORDER BY MIN(o)
IF (#AuthType IS NOT NULL AND #AuthType = 'allow')
SET #ret = 1;
RETURN #ret;
END;
That function splits up the xml allow and deny nodes into a table which contains the authorization type (allow or deny), the users list, the roles list, and finally the order in which the particular node appears in the document. Finally, i can grab the first node where i find the user or one of the user's roles. If that node is "allow", then i return a 1.
Yeah, it's a bit horrendous because we're declaring a table in every single call. I tried various little tests where i only looked for the user name (to avoid having to make any calls to the ufnContainsAny), but the performance didn't change. I also tried changing the "o" column to a simple identity column, since i'm selecting all nodes - this would allow it to skip what i thought might be a time consuming calc of getting the order of the node. But that also didn't affect the performance.
So, not surprisingly this method needs work. If anyone has any suggestions, i'm all ears.
My initial usage of this feature will be very few rows, so i can use this in the interim until i come up with a better solution (or abandon this method altogether).
EDIT:
The performance can be dramatically improved by just skipping the DECLARE table / INSERT. Instead, we can do this:
SELECT TOP 1 #AuthType = a FROM
(
SELECT
a = value.value('local-name(.[1])', 'varchar(32)'),
u = ',' + value.value('#users', 'varchar(max)') + ',',
r = ',' + value.value('#roles', 'varchar(max)') + ',',
o = value.value('for $i in . return count(../*[. << $i]) + 1', 'int')
FROM #SecurityRules.nodes('//allow,//deny') AS T(value)
) AS sec
WHERE CHARINDEX(',' + #UserName + ',', u) > 0 OR CHARINDEX(',*,', u) > 0 OR dbo.ufnContainsAny(r, #UserRoles) > 0 OR CHARINDEX(',*,', r) > 0
GROUP BY a
ORDER BY MIN(o)
I have multiple databases. Can I hit one of the databases based on an identifier which is dynamic?
e.g. I have three databases DB1,DB2,DB3.
I have a query select * from tblEmployees.(This table is present in all the three DBs). I have an identifier(or some variable ) whose value can be 1 or 2 or 3 and based on the value of this variable which I get dynamically when my service is hit, I would like to choose the DB from which the values should be obtained.
Can this be done? My DB is SQL Server and front end is asp.net.
My connection strings are stored in web.config file. Can I have multiple connection string which will have the same server with diff db names and select one of them based on the identifier.
1.In real world most of the time you have to store your connection strings in your web.config file .
so there you can let's keep three connection strings which will have the same server but different databases name, then you can select one of the connection-string for your app connection to the required database.
2.you can build that connection string on run-time if you need.
using these technique you will never have to write 2 or more queries just change the query string and your queries will work for all the databases.
You can do like this
if(val == 1)
{
select * from [DB1].[dbo].[tblEmployees]
}
else if(val == 2)
{
select * from [DB2].[dbo].[tblEmployees]
}
Try this one -
DECLARE #ID INT
SELECT #ID = 2
DECLARE #SQL NVARCHAR(500)
SELECT #SQL = 'SELECT * FROM DB' + CAST(#ID AS CHAR(1)) + '.dbo.tblEmployees'
PRINT #SQL
EXEC sys.sp_executesql #SQL
Output -
SELECT * FROM DB2.dbo.tblEmployees
T-SQL way:
declare #db int
if #db = 1
begin
use [db1]
select *
from tblEmployees
end
if #db = 2
begin
use [db2]
select *
from tblEmployees
end
-- and so on
IMO you're best bet would be to use a different connection-string to achieve multi-tenency against similar alike databases. Ideally, abstracted away behind some code so that most of your code doesn't need to know about it, but just does:
using(var conn = Somewhere.GetOpenConnection()) {
// ...
}
or worst-case:
using(var conn = Somewhere.GetOpenConnection(Environment.Published)) {
// ...
}
(here Environment is an enum to what the various databases represent)
where GetOpenConnection figures out which database is needed, and either looks up on constructs the correct connection string.
But to be specific:
you cannot parameterize the DB name in a query
using use between operations would be a really bad idea in terms of connection re-use
you can explicitly use three-part identifiers (i.e. DB1..SomeTable or DB1.dbo.SomeTable), but that does not scale naturally to lots of databases
Im using such a query in my stored procedure
SET #Statement =
'SELECT Id,Title,Content,Status,ROW_NUMBER()
OVER (ORDER BY ' + #Sort + ') AS StudentReport
FROM YearBook
WHERE ' + #Criteria + ')
AS ArticleNumber
WHERE StudentReport> ' + CONVERT(NVARCHAR, #StartRowIndex) + ' AND
StudentReport<= (' + CONVERT(NVARCHAR, #StartRowIndex + #MaximumRows);
Just want to know whether its possible to do sql injection to this stored procedure. If yes, how can i prevent it? Need Help !!!
Yes it's possible. Quite easy, even. Try setting
#Criteria = "\r\nGO\r\nexec sp_addlogin 'hacker', 'broken'\r\nGO";
The batch will product errors, but the part in between will run nevertheless so welcome your new login.
The correct way to do your query could be something like this.
CREATE PROC FindSomething
#StartRowIndex int,
#MaximumRows int,
#Sort int, -- 1-4 representing the columns, say in a dropdown
#Id int,
#Content varchar(max),
#Title varchar(max),
#Status int
AS
SELECT Id,Title,Content,Status
FROM
(
SELECT Id,Title,Content,Status,
ROW_NUMBER() OVER (ORDER BY
CASE when #Sort = 1 then Id
when #Sort = 4 then Status
end,
CASE when #sort = 2 then Title
when #sort = 3 then Content
end) AS StudentReport
FROM YearBook
WHERE (#id is null or #id = Id)
AND (#Content is null or #Content = Content)
AND (#Title is null or #Title = Title)
AND (#status is null or #Status = Status)
) Numbered
WHERE StudentReport >= #StartRowIndex
AND StudentReport <= #StartRowIndex + #MaximumRows
OPTION (RECOMPILE);
GO
Read here on more about dynamic searching: www.sommarskog.se/dyn-search.html
Note: I split up 1/4 and 2/3 in the sort because each branch of a CASE statement must produce the same type, or that is compatible. int/varchar is very bad mix to have in a case statement.
Assuming the above is a string that you are building and then executing with EXEC or sp_executesql then Yes, SQL injection is possible.
How to prevent it depends on what you are trying to do. Perhaps you need to rethink your approach.
Yes, it will. You can still do things to help defend against it tho'
For example, #Sort is a column name, so you can escape that properly (and ensure that if someone tried to inject something into is it won't work, because it has been properly escaped. For that use QUOTENAME.
QUOTENAME(#Sort)
#Criteria is more difficult because you are actually expecting a fragment on SQL code so it becomes very difficult to work out what is valid and what is malicious. You might want to reconsider what you are trying do do here. If you must use Criteria then ensure that the security model is set up so that only the application(s) that absolutely needs it has access to the stored proc that does this. Make sure that the validation in the application before it sends of the SQL to ensure that anything it is doing isn't going to be damaging.
It looks like you are trying to make a pretty generic search stored procedure with paging. These are difficult to implement properly in t-sql only, and can become maintenance headaches down the road due to the branching logic, or additional supporting stored procedures you need to add...
I would start to look at other options outside of a pure sql approach. Using an orm, or micro orm could help a lot. Actually, take a look at what Sam Saffron came up with...
http://samsaffron.com/archive/2011/09/05/Digging+ourselves+out+of+the+mess+Linq-2-SQL+created
I have an ASP.NET MVC 3 app that lets users to create their own filters. Something like amount > 5 and amount <= 7, and so on. The user can choose both the amount value and the operator.
My problem is how to pass those filters to the stored procedure that retrieves the data. The stored procedure is already pretty complicated, meaning there are a lot of parameters passed which are checked to be null, so I can't really apply the answer I found here: T-SQL Stored Procedure - Dynamic AND/OR Operator
Is there any other way I can do this?
Operators cannot be parameterized. Since you mention this is a stored procedure, the only option is to write the T-SQL inside the SP, and use sp_executesql, i.e.
//TODO : verify (whitelist) that #operator is in a known set of values...
// '=', '<>', '>', '<' etc - otherwise security risk
declare #sql varchar(4000) = 'select * from Foo where Bar '
+ #operator + ' #value'
exec sp_executesql #sql, N'#value int', #value
This builds the query (#sql) on the fly, but keeps the value (#value) parameterized throughout, so we only need to white-list the operator (#operator).
Just to show how the value remains parameterized, we could also have used:
//TODO : verify (whitelist) that #operator is in a known set of values...
// '=', '<>', '>', '<' etc - otherwise security risk
declare #sql varchar(4000) = 'select * from Foo where Bar '
+ #operator + ' #p'
exec sp_executesql #sql, N'#p int', #value
Here, #p is the name of the parameter in the inner sql, and #value is the name of the parameter in the stored procedure; the third/fourth/fifth/etc parameters to sp_executesql are mapped to the parameters declared in the second parameter to sp_executesql (which in this example, declared just #p).
Note that if this wasn't a stored procedure, you could perform the query-construction step in C#, again keeping the value as a parameter.
I think the only way is to use dynamic sql, e.g., sq_executesql. This allows you to create your SQL statement as a string, and then execute it. But it will probably mean a lot of rewriting of your existing stored procedure!
Sql Server 2008 Express >> Visual Web Developer >> C#
I'm pulling records out of the table like this:
SELECT Name, Category, Review FROM ReviewTable
This works fine but the Review field Type in SQL Server is text and is very long (think a magazine article).
I only want to pull the first four lines from the Review field for each row, and display them in my repeater control. These lines will be like a teaser of the article.
Is this possible? How can it be accomplished?
This will return this first 1000 characters from the review.
SELECT Name, Category, CAST(Review AS VARCHAR(1000) FROM ReviewTable
If you must have the first 4 lines you need to use some split function. This could work:
CREATE FUNCTION [dbo].[Split]
(
#SearchString VARCHAR(8000),
#Separator VARCHAR(5),
#MaxItems INT
)
RETURNS #strtable TABLE (strval VARCHAR(8000))
AS
BEGIN
DECLARE #tmpStr VARCHAR(8000), #intSeparatorLength INT, #counter int
IF #MaxItems IS NULL
SET #MaxItems = 2147483647 -- max int
SET #intSeparatorLength = LEN(#Separator)
SET #Counter = 0
SET #tmpStr = #SearchString
WHILE 1=1 BEGIN
INSERT INTO #strtable VALUES ( SUBSTRING(#tmpStr, 0 ,CHARINDEX(#Separator, #tmpStr)))
SET #tmpStr = SUBSTRING(#tmpStr,CHARINDEX(#Separator,#tmpStr)+LEN(#Separator),8000)
SET #counter = #counter + 1
IF (CHARINDEX(#Separator,#tmpStr) < 1 OR #counter >= #MaxItems)
BREAK
END
RETURN
END
Usage: select * from dbo.split('aaa**bbbb**CCCC**dddd**eeee**dffff**ggggg', '**', 4)
Well ,the first for lines may be a bit more difficult, but why don't you just put out the first 250 characters or so?
SELECT Name, Category, SubString(Review, 1, 250) AS Review FROM ReviewTable
If your database server is in the same local network as your web server, I think I'd probably select the entire field, since you're accessing it at all. You'll still have to do a lookup to access any data in that field, so sql performance-wise for finding the data is a non-issue. The only downside of retrieving the entire field would be the amount of data passed between the servers. Thus: if they're in the same network, I'd say this would definitely be cheaper than tampering with each record during selection. It also gives you the ability to cache your response, so that you don't have to hit the database again when the user wants to see the full version of the text.
But, to answer your question, the below should probably do it, altho it looks rather tacky
SELECT Name, Category, left(convert(varchar(8000), Review), charindex('\n', convert(varchar(8000), Review), charindex('\n', convert(varchar(8000), Review), charindex('\n', convert(varchar(8000), Review), charindex('\n', convert(varchar(8000), Review))+1)+1)+1)-1) FROM ReviewTable
...hrrm, yeah, really, i'd consider my first paragraph