+3  A: 

Why do this in the stored procedure? A better solution might be to attack this on the client side, escaping strings and checking lengths before calling the stored proc. Libraries like the MS Enterprise DAAB (.NET) provide convenient ways to do this by specifying datatype and length of parameters when you add them to the command object.

David Lively
...or simply use LinqToSQL.
Adrian Grigore
Good point about doing this outside of a stored proc. In .NET 3.5 I do this by building the query up using LINQ- very easy to do.
RichardOD
@DyingCactus- look at this: http://www.albahari.com/nutshell/predicatebuilder.aspx
RichardOD
Thanks. I am aware of client-side approaches (although I haven't had the pleasure of using LinqToSql yet). However, if it was necessary to run dynamic sql in a stored proc I wanted to know if the example was safe.@RichardOD: Thank you for the link, I will look at it.
DyingCactus
+1  A: 

You don't need dynamic SQL to make optional WHERE clauses... just use:

WHERE ((@x IS NULL) OR (@x = ...)) AND ...

Should be faster also, and no risk for overflowing strings, injection, or whatever.

Like this:

CREATE PROCEDURE ExecutePeopleFilter
  (
   @lastNameFilter varchar(20),
   @companyNameFilter varchar(20),
   @ageFilter int,
   @dateFilter datetime
  )
AS 
  BEGIN
    SELECT FirstName, LastName, CompanyName, Age, StartDate
      FROM People
      WHERE (
             (ISNULL(@lastNameFilter, '') = '')
             OR (LastName LIKE @lastNameFilter+'%')
            )
        AND (
             (ISNULL(@companyNameFilter, '') = '')
             OR (LastName LIKE @companyNameFilter+'%')
            )
        AND (
             (@ageFilter IS NULL)
             OR (Age = @ageFilter)
            )
        AND (
             (@dateFilter IS NULL)
             OR (StartDate = @dateFilter)
            ) ;
  END
Lucero
Faster? "OR" in WHERE clauses is pretty slow.
Joel Coehoorn
If you look at the execution plan, you'll see that this is handled pretty well. Faster because there is no building, parsing and compilation of the query involved for each call.
Lucero
Okay, so I created a little benchmark on my local SQL Server instance on my workstation. Inserted 10'000'000 rows (ten millions) of random data with a generator; queries on the SP with any mix of used and NULL parameters return in 00:00:00 in the query analyzer. So I guess that this approach is not "pretty slow", is it?
Lucero
@Lucero: Thanks, I've used this technique in other projects and have found that for more complex logic (especially in the where-clause) than the simplified example I gave in the question, the code becomes harder to maintain and/or optimize. Perhaps for simple where-clauses and for logic that only involves one select statement, the performance with the OR-method may be better or the same. However, I wanted to know if the technique given in the example was safe.
DyingCactus
@DyingCactus: My experience is exactly the opposite. Using dynamically build SQL statements has more downsides, such as possible syntax problems in certain cases, strings which get to large (no in your simple sample above, but if the amount of WHERE's added to the query is larger this can become a "indeterministic" problem), tools for refactoring (such as renaming columns etc.) and analysis (such as finding dependencies) don't work with dynamic code. Also, I like the instant syntax check on creation or alter of a SP very much, it prevents invalid names/typos to get into the code.
Lucero
@Lucero: You are right on all your points regarding disadvantages of dynamic sql.
DyingCactus
+2  A: 

Pretty much.

The key to preventing SQL injection is correct handling of parameters via an "approved" mechanism and avoiding string concatenation.

Your code does not build up a string with the parameters: they are separated and cleaned via sp_executesql.

Whether you'd do it this way is a different matter... as other answers show

gbn
Marked as answer because it was first one to directly answer the question. Still, answers by others also offer valuable input.
DyingCactus
+1  A: 

You never concatenate anything besides well-known, hard-coded values into a SQL statement to be issued to the database engine, so it is safe from all currently known SQL injection approaches (and should be robust against future attacks). It does, however, have other problems (like @startDate not being declared).

Nicole Calinoiu
Thanks but isn't @startDate declared in the "set @params" statement?
DyingCactus
+1  A: 

Yes, your use of string concat and sp_executesql is correct and SQL injections won't be an issue.

And no, just because LINQ is the new hotness doesn't mean it is the right solution.

That said, your varchar(20) parameters can easily overflow, you may want to bump those up a bit, to at least the size of the actual field.

richardtallent
Thanks. I agree with your LINQ comment. In any case, even though there are better ways to get the results of the example, I wanted to know if the technique was safe for use in places where other methods are not practical.
DyingCactus
A: 

I think so. At least, it looks okay. But it's probably not how I'd go about it.

One of the key tenants of security is never never never ever write your own security code. You always want to lean as much as possible on the mechanisms provided by and built into your platform. You're doing that somewhat via your use of sp_executesql, but I expect you could do more as well.


By request, I'd probably do it more like this:

create procedure ExecutePeopleFilter 
    (@lastNameFilter varchar(20) = NULL, 
    @companyNameFilter varchar(20) = NULL, 
    @ageFilter int = NULL, 
    @dateFilter datetime = NULL)
as
begin

    if (LEN(@lastNameFilter) < 20) set @lastNameFilter += '%'
    if (LEN(@companyNameFilter) < 20) set @companyNameFilter += '%'

    SELECT FirstName, LastName, CompanyName, Age, StartDate 
    FROM People
    WHERE  LastName    LIKE COALESCE(@lastNameFilter, LastName) 
       AND CompanyName LIKE COALESCE(@companyNameFilter, CompanyName)
       AND Age          =   COALESCE(@ageFilter, Age)
       AND StartDate    =   COALESCE(@dateFilter, StartDate)

end

No dynamic sql needed, and no "OR"s in the WHERE clause.

Joel Coehoorn
Thanks. Can you please describe how you'd go about it?
DyingCactus
The coalesce version looks nice but will not work if there are rows with null values for any of the filtered columns. For example: if @companyNameFilter = 'X' and @dateFilter is null and there is a row for that company but its StartDate is null, the row will not get included in the result. But if all your filtered columns are NOT-NULL, this is a good replacement for the dynamic sql version instead of the OR-method.
DyingCactus
You can COALESCE() the left side, too, but then you're down to "OR" like performance. So this can work depending on what your data looks like.
Joel Coehoorn
Also, keep in mind that this is _one_ alternate way to do it. Another is to use case statements.
Joel Coehoorn
A: 

Even if it is... "ugh".

Why not instead use a temp table, populate it with the IDs of the element results that match your non-optional parameters and then eliminate the rest from the temp table based on the parameters that have been specified? Once you've done that just join on the result set you're looking for.

CREATE TABLE #People
   (personid int)
INSERT INTO #People SELECT personid FROM people
IF NOT @lastNameParam IS NULL
   DELETE FROM #People WHERE personid NOT IN (SELECT personid FROM people WHERE lastname LIKE @lastNameParam + '%')
-- And so on...
Spencer Ruport
Can you please explain what "ugh" means here? Is the code technique wrong, style wrong, efficiency poor, or just looks ugly? I am not sure how much better an explicit temp table will perform especially with large tables and/or more complex logic than the simple example given. Unless absolutely necessary, I'd rather let sql decide/figure-out at the low-level how to get the data.
DyingCactus