SQL Injection: How It Works and How to Stop It

SQL injection happens when untrusted input becomes part of a command’s structure. Parameters keep values separate from SQL syntax, which is the central fix rather than a clever blacklist.

A plain wooden sorting box with separate compartments and a brass latch on a softly lit table.

The Boundary Is Between Data and Syntax

A search term should be a value the query compares with a column. If code pastes that term into the statement, quote characters can change how the statement is parsed. The application has handed input control over part of its grammar.

The problem can arise in application code or inside a stored procedure. Using a stored procedure does not automatically make concatenated dynamic SQL safe. Review how the final command is constructed.

DECLARE @Search nvarchar(100) = N'O''Brien';
DECLARE @UnsafeSql nvarchar(max) =
    N'SELECT name FROM sys.objects WHERE name = N''' + @Search + N''';';
SELECT @UnsafeSql AS constructed_text;

This example only displays the constructed text. It shows how an ordinary apostrophe already disrupts the intended quoting. You do not need an elaborate attack string to see that the boundary is wrong.

Pass Values as Parameters

With sp_executesql, put placeholders in the command and declare their types separately. Supply the values through the parameter arguments. The command structure stays fixed when the search value changes.

DECLARE @Search nvarchar(128) = N'O''Brien';
EXEC sys.sp_executesql
    N'SELECT name, type_desc
      FROM sys.objects
      WHERE name = @Name;',
    N'@Name nvarchar(128)',
    @Name = @Search;

The same principle applies through application drivers. Bind parameters with appropriate SQL types and lengths instead of formatting values into strings. Dates, numbers, and binary values benefit from that separation too.

Calling sp_executesql with an already concatenated unsafe string does not repair it. Its name is not a protective wrapper. The protection comes from keeping untrusted values out of the command text.

Treat Identifiers as a Separate Problem

Parameters represent values, not table names, column names, or keywords. A dynamic identifier therefore needs a different approach. Prefer a small approved mapping when users choose among known options.

DECLARE @SortChoice nvarchar(20) = N'name';
DECLARE @Column sysname =
    CASE @SortChoice WHEN N'name' THEN N'name'
                     WHEN N'type' THEN N'type_desc' END;
IF @Column IS NULL
    THROW 50001, 'Unsupported sort choice.', 1;
DECLARE @Sql nvarchar(max) =
    N'SELECT name, type_desc FROM sys.objects ORDER BY '
    + QUOTENAME(@Column) + N';';
EXEC sys.sp_executesql @Sql;

The mapping decides what is permitted, and QUOTENAME makes the chosen identifier syntactically safe. These are separate responsibilities. Quoting an arbitrary object name does not establish that the caller should be allowed to access it.

QUOTENAME accepts identifiers up to 128 characters and returns NULL for longer input. Validate input before narrowing it into a smaller buffer. Silent truncation can defeat assumptions made earlier in the code.

Keep Values Parameterized in Dynamic Shapes

A statement can have a validated dynamic sort column and still parameterize its filter. Do not abandon value parameters merely because one structural element varies. Construct only the part that genuinely needs to change.

DECLARE @Column sysname = N'name';
IF @Column NOT IN (N'name', N'type_desc') OR @Column IS NULL
    THROW 50002, 'Unsupported column.', 1;
DECLARE @Sql nvarchar(max) =
    N'SELECT name, type_desc FROM sys.objects
      WHERE type = @ObjectType ORDER BY ' + QUOTENAME(@Column) + N';';
EXEC sys.sp_executesql @Sql,
    N'@ObjectType char(2)', @ObjectType = 'U';

For a sort direction, choose between fixed ASC and DESC tokens through an approved mapping. Do not paste a free-form direction string into the command. Similar rules apply to selectable operators and optional query fragments.

Reduce the Damage Available to a Bug

The application account should have only the permissions its work requires. A narrow permission set does not remove an injection defect, but it limits reachable operations. Separate deployment privileges from ordinary runtime access.

Input validation remains useful for business rules and reasonable lengths. It is not a replacement for parameterization. Removing selected punctuation often rejects legitimate names while missing other unsafe constructions.

Also review values read from stored data before reusing them in dynamic SQL. A value does not become trusted merely because it was saved earlier. Keep the same data-versus-syntax boundary at every construction point.

Test the Contract at Its Edges

Test apostrophes, Unicode text, empty values, and maximum supported lengths through the actual application path. Confirm that invalid structural choices are rejected clearly. Verify both the result and the parameter metadata reaching SQL Server.

Search code for concatenation near EXEC, sp_executesql, and driver command creation. Review each case rather than assuming every occurrence is unsafe. The goal is an explainable boundary, not a keyword count.

Injection prevention is not punctuation cleanup, it is keeping data out of command syntax.

This post was rewritten from scratch in September 2026. The original, published on 2010-03-10, was a short announcement about something that no longer exists. The address is the same, the subject is now something worth keeping.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.

Best Practices, Database, SQL Server, SQL Server Security
Previous Post
Replacing Old Syntax in Legacy T-SQL
Next Post
Connecting Java Applications With the JDBC Driver

Related Posts

6 Comments. Leave new

  • Feodor Georgiev
    March 10, 2010 11:35 am

    Yes, I agree. This book became one of my favorite SQL Server books about 1 day after I bought it. I would recommend it to any (accidental or not) DBA.

    Reply
  • chandrakant Singh
    March 10, 2010 3:21 pm

    I want To Display Top 1 record From Member Wise

    MemberId Details Date
    2 3369.56-0 03/04/2010
    2 0.00-1001 NULL
    2 0.00-30 NULL
    2 0.00-45 NULL
    2 0.00-60 NULL
    4 2300.00-1001 03/08/2010
    4 2577.95-0 12/16/2009
    4 0.00-45 NULL
    4 2559.91-30 NULL
    4 731.43-60 NULL
    14 0.00-1001 NULL
    14 0.00-30 NULL
    14 0.00-45 NULL
    14 0.00-60 NULL

    Kindly give me a solution

    Reply
  • chandrakant Singh
    March 10, 2010 3:23 pm

    I want To Display Top 1 record From Member Wise

    MemberId Details Date
    2 3369.56-0 03/04/2010
    2 0.00-1001 NULL
    2 0.00-30 NULL
    2 0.00-45 NULL
    2 0.00-60 NULL
    4 2300.00-1001 03/08/2010
    4 2577.95-0 12/16/2009
    4 0.00-45 NULL
    4 2559.91-30 NULL
    4 731.43-60 NULL
    14 0.00-1001 NULL
    14 0.00-30 NULL
    14 0.00-45 NULL
    14 0.00-60 NULL

    Kindly give me a solution

    Reply
    • Hi,
      Do you want just the top member id or the top member id within each diff member id’s (like 2,4,14)

      Thank you

      Reply
  • chandrakant Singh
    March 11, 2010 10:48 am

    Thank You Ramdas

    I Got The Output By

    With Cte As(Select MemberId,Details,Date,Row_Number() Over(Partition By MemberId Order By MemberId) Rn From TableName
    )
    Select * From Cte Where Rn=1

    Thanks & Regards
    Chandrakant Singh

    Reply
  • You will have basically made some extremely good points right here. I specifically value the way in which you’ve been ready to stick so much thought right into a fairly short publish (comparitively) which results in it an thoughtful publish in your topic.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.