sp_executesql error with DDL statements (preventing SQL injection)

One of the requirement for an application I'm currently
working on is for the end user of a web application/site to be able to create
objects in the database.

There are a number of objects that must be created or
manipulated but for the sake of simplicity let's take for example creating a table with one
column. The SQL statement would look like:

CREATE TABLE [MyTable]
( [MyColumn] INT NOT NULL )

To minimize SQL injection, I was hoping I could use the sp_executesql stored procedure to come up with a
parameterized query (in addition to other preventive measures like validating input). Something like:

DECLARE @SQLString NVARCHAR(MAX);
DECLARE @ParamDefinition NVARCHAR(256);

SET @SQLString =
  N'CREATE
TABLE @TableName (@ColumnName INT NOT NULL)';
SET
@ParamDefinition =
  N'@TableName VARCHAR(128),
@ColumnName VARCHAR(128)';

EXECUTE sp_executesql
 
@SQLString,
  @ParamDefinition,
  @TableName = 'MyTable',
 
@ColumnName = 'MyColumn';

When I executed the statement it returned an error
"Incorrect syntax near
'@TableName'
". Though I haven't worked with sp_executesql with
Data Definition Language (DDL) statements I have been using it for Data
Manipulation Language (DML) statements such as conditional selects and others so
I'm quite sure there is nothing wrong with syntax near @TableName by itself.
What could be wrong?[more]

It turns out that using sp_executesql with DDL
statements are not supported.

So in this particular case, to minimize SQL injection
you have to use concatenation to dynamically build the SQL statement.

DECLARE @SQLString NVARCHAR(MAX)
DECLARE @TableName VARCHAR(128)
DECLARE @ColumnName VARCHAR(128)
SET @TableName = 'MyTable'
SET
@ColumnName = 'MyColumn'
SET @SQLString =
 'CREATE TABLE ' + @TableName + '( ' + @ColumnName + ' INT
NOT NULL)'
EXEC (@SQLString)

But wait, there is something else you can do to prevent
SQL injection so all is not really hopeless. Meet QUOTENAME. As you might have noticed before,
one way to create a table with spaces in the name is using quotes or brackets
like CREATE TABLE [hello world how are you]. So anything between the delimiter
(eg. quote, brackets) are considered part of the TableName rather than a
command/statement.

When you experiment more you will notice that the
following statement will create a table named as indicated instead throwing an
error or doing something messy: 

CREATE TABLE [Hello; Drop Master; ] ( [MyColumn] INT NOT
NULL)

Yes I know that's a very clever sql injection attempt
but here's another example:

DECLARE @SQLString NVARCHAR(MAX)
DECLARE @TableName VARCHAR(128)
DECLARE @ColumnName VARCHAR(128)
SET @TableName = 'MyTable (ColA INT NULL); PRINT ''HELLO'';
— '
SET @ColumnName = 'MyColumn'
SET @SQLString =
 'CREATE TABLE '
+ @TableName + '( ' + @ColumnName + ' INT NOT NULL)'
PRINT (@SQLString)

Try executing that in your table and
along with creating the table named MyTable you will see HELLO printed in the
output/messages window. If you replace the print statement with a more
maliciously statement such as  DROP DATABASE master and you happen to be running
a highly privileged account then you're in trouble.

But try the statement again but this
time using this @SQLString:

SET @SQLString =
 'CREATE TABLE ' + QUOTENAME(@TableName) + '( ' +
QUOTENAME(@ColumnName) + ' INT NOT NULL)'

As you might have expected you should get a
table named [MyTable (ColA INT NULL); PRINT 'HELLO'; — ] instead of HELLO being
printed. Should there be a '[' or ']' in your variable value, it will be changed
to '[[' and ']]' respectively just like how single quotes are changed to two
single quotes to avoid issues.

Now that's seems a lot better.

So in addition to other ways of
protecting your database such as (but not limited to) validating input (very
well), running least privileged SQL server account, using stored procedures
(along with determining proper parameter data type and length) when faced with
the need to perform dynamic queries where you cannot use sp_executesql like in this case with DDL
statements, see if QUOTENAME would be
applicable and if it would help.

** note that QUOTENAME(dbo.Employee)
will not result to [dbo].[Employee] but rather [dbo.Employee] so for fully
qualified names, quote the server/database/schema and table names
respectively.