Dynamic ORDER BY Without Injection: Whitelisting Sort Columns

A dynamic ORDER BY is safe when the sort column comes from a short approved list. The user picks a name, you check it against the list, and only then does it touch the SQL string. Values still travel as parameters.

A pastry crimper seals a pie edge through its fixed toothed shape beside unused decorative cutters

Why ORDER BY is the awkward one

Parameters work for values. They do not work for column names. You cannot write ORDER BY @SortColumn and expect a sort by that column. So many grids that let users click a header end up gluing the header name straight into a string. That is where trouble starts.

Imagine a grid on an internal screen. A curious user edits the request and sends a sort name that is not a column at all. If the code trusts it, that text becomes part of your statement. First, a small table to play with.

DROP TABLE IF EXISTS dbo.SortDemo;
CREATE TABLE dbo.SortDemo (Id int PRIMARY KEY, Name nvarchar(50), Amount decimal(12,2));
INSERT dbo.SortDemo VALUES (1, N'Alpha', 10), (2, N'Beta', 20), (3, N'Gamma', 20);

See the bad statement without running it

The next block only builds statements and shows them. Nothing runs. The first column is what naive concatenation produces for a hostile sort name. The second shows what QUOTENAME does to the same input.

DECLARE @SortColumn nvarchar(128) = N'Amount; DROP TABLE dbo.SortDemo';

SELECT N'SELECT Id, Name, Amount FROM dbo.SortDemo ORDER BY ' + @SortColumn AS NaiveStatement,
       N'SELECT Id, Name, Amount FROM dbo.SortDemo ORDER BY ' + QUOTENAME(@SortColumn) AS QuotedStatement;

The naive statement contains a second command after the semicolon. The quoted one wraps everything in square brackets, so it becomes one odd column name, and the query would fail instead of running the extra command. That helps, but QUOTENAME only delimits an identifier. It does not decide whether the identifier is allowed. The approved list does that job.

Check the list, then build the statement

Here is the whole pattern as a procedure. Three rules: the column must be on the list, the direction must be ASC or DESC, and the amount filter stays a real parameter. Id is a tie breaker, so rows with equal amounts always come back in the same order.

CREATE OR ALTER PROCEDURE dbo.SortedList
    @SortColumn sysname, @Direction varchar(20), @MinAmount decimal(12,2)
AS
BEGIN
    SET NOCOUNT ON;
    IF @SortColumn IS NULL OR @SortColumn NOT IN (N'Id', N'Name', N'Amount')
        THROW 50000, N'Choose a supported sort column.', 1;
    IF @Direction IS NULL OR @Direction NOT IN ('ASC', 'DESC')
        THROW 50000, N'Choose ASC or DESC.', 1;

    DECLARE @sql nvarchar(max) =
        N'SELECT Id, Name, Amount FROM dbo.SortDemo WHERE Amount >= @MinAmount ORDER BY '
        + QUOTENAME(@SortColumn) + N' ' + @Direction + N', Id;';
    EXEC sys.sp_executesql @sql, N'@MinAmount decimal(12,2)', @MinAmount = @MinAmount;
END;

Now call it twice. The first call is a normal request. The second sends a hostile sort name.

EXEC dbo.SortedList @SortColumn = N'Amount', @Direction = 'DESC', @MinAmount = 0;

BEGIN TRY
    EXEC dbo.SortedList @SortColumn = N'Amount;SELECT 1', @Direction = 'DESC', @MinAmount = 0;
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER() AS RejectedInputError, ERROR_MESSAGE() AS RejectedInputMessage;
END CATCH;
Approved sort results and the rejected unsupported sort name
Approved ordering returns Beta, Gamma, then Alpha. An unsupported sort name raises the caught error 50000.

The good call returns Beta, Gamma, then Alpha. Beta and Gamma both have 20, so Id decides, and Beta comes first. The hostile call never reaches the query. It stops at the list with error 50000 and a message a user can understand.

Four rules for a sortable grid

Try the other inputs

EXEC dbo.SortedList @SortColumn = N'Name', @Direction = 'ASC', @MinAmount = 15;

BEGIN TRY
    EXEC dbo.SortedList @SortColumn = N'Name', @Direction = 'DESC; SELECT 1', @MinAmount = 0;
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_MESSAGE() AS Problem;
END CATCH;

The first call sorts by name and filters out Alpha, so you get Beta and Gamma. The second tries to sneak something into the direction. The direction check catches it, and the message says to choose ASC or DESC. I made the direction parameter wide enough to hold the junk, so the check sees the whole text. Also check NULL and empty input, because a missing value should fail the same way. And treat a saved screen preference like any other input. Storing it does not make it trusted.

A static alternative

If you only need two or three sort choices, you can skip dynamic SQL. A CASE expression picks the sort inside a normal query. Keep one CASE per data type, so each sort keeps its own type.

DECLARE @SortColumn nvarchar(20) = N'Name';

SELECT Id, Name, Amount
FROM dbo.SortDemo
ORDER BY CASE WHEN @SortColumn = N'Name' THEN Name END,
         CASE WHEN @SortColumn = N'Amount' THEN Amount END DESC,
         Id;

With Name chosen you get Alpha, Beta, Gamma. I like the CASE version for tiny lists and the approved-list version when there are many columns. Test the plans on real filters, then choose the simpler one that behaves.

Clean up

DROP PROCEDURE IF EXISTS dbo.SortedList;
DROP TABLE IF EXISTS dbo.SortDemo;

Next time a grid asks for a custom sort, make the list short and make it yours.

A sort name is not trusted SQL, it is an identifier to approve.

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


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

Dynamic SQL, SQL Order By, SQL Server Security, SQL Variable
Previous Post
SQL SERVER – Quiz and Video – Introduction to SQL Server Security
Next Post
Timed-Out Queries in Query Store: Finding Aborted Executions

Related Posts

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.