Building SQL queries by concatenating strings allows attackers to inject arbitrary SQL code, potentially reading, modifying, or deleting any data in the database.
public User GetUser(string username)
{
string query = "SELECT * FROM Users WHERE Username = '" + username + "'";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
connection.Open();
var reader = command.ExecuteReader();
// Process results...
}
}
// Attacker sends: "admin' OR '1'='1"
// Resulting query: SELECT * FROM Users WHERE Username = 'admin' OR '1'='1'
// Returns all users!public User GetUser(string username)
{
string query = "SELECT * FROM Users WHERE Username = @Username";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
// Use parameterized query
command.Parameters.AddWithValue("@Username", username);
connection.Open();
var reader = command.ExecuteReader();
if (reader.Read())
{
return new User
{
Id = reader.GetInt32(0),
Username = reader.GetString(1),
Email = reader.GetString(2)
};
}
return null;
}
}Even when using some parameters, combining them with string concatenation still creates injection vulnerabilities.
public List<Product> SearchProducts(string category, string minPrice, string maxPrice)
{
string query = "SELECT * FROM Products WHERE Category = @Category";
// Dangerous: Concatenating WHERE clauses
if (!string.IsNullOrEmpty(minPrice))
{
query += " AND Price >= " + minPrice;
}
if (!string.IsNullOrEmpty(maxPrice))
{
query += " AND Price <= " + maxPrice;
}
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Category", category);
connection.Open();
// Execute query...
}
}
// Attacker sends minPrice: "0 OR 1=1; DROP TABLE Products--"public List<Product> SearchProducts(string category, decimal? minPrice, decimal? maxPrice)
{
var queryBuilder = new StringBuilder("SELECT * FROM Products WHERE Category = @Category");
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand())
{
command.Connection = connection;
command.Parameters.AddWithValue("@Category", category);
// Add optional parameters safely
if (minPrice.HasValue)
{
queryBuilder.Append(" AND Price >= @MinPrice");
command.Parameters.AddWithValue("@MinPrice", minPrice.Value);
}
if (maxPrice.HasValue)
{
queryBuilder.Append(" AND Price <= @MaxPrice");
command.Parameters.AddWithValue("@MaxPrice", maxPrice.Value);
}
command.CommandText = queryBuilder.ToString();
connection.Open();
var products = new List<Product>();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
products.Add(new Product
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Category = reader.GetString(2),
Price = reader.GetDecimal(3)
});
}
}
return products;
}
}You cannot use parameters for table or column names in SQL. Building queries with user-controlled table/column names requires careful whitelisting.
public DataTable GetDataFromTable(string tableName, string columnName)
{
// Parameters don't work for table/column names!
string query = $"SELECT {columnName} FROM {tableName}";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
connection.Open();
var adapter = new SqlDataAdapter(command);
var dataTable = new DataTable();
adapter.Fill(dataTable);
return dataTable;
}
}
// Attacker sends: tableName = "Users; DROP TABLE Users--"public DataTable GetDataFromTable(string tableName, string columnName)
{
// Whitelist allowed tables and columns
var allowedTables = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Products", "Orders", "Customers"
};
var allowedColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Id", "Name", "Description", "Price", "Category"
};
// Validate inputs against whitelist
if (!allowedTables.Contains(tableName))
{
throw new ArgumentException("Invalid table name");
}
if (!allowedColumns.Contains(columnName))
{
throw new ArgumentException("Invalid column name");
}
// Use QUOTENAME to properly escape identifiers
string query = $"SELECT QUOTENAME({columnName}) FROM QUOTENAME({tableName})";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
connection.Open();
var adapter = new SqlDataAdapter(command);
var dataTable = new DataTable();
adapter.Fill(dataTable);
return dataTable;
}
}
// Better approach: Use a mapping dictionary
private static readonly Dictionary<string, string> TableMap = new Dictionary<string, string>
{
["products"] = "dbo.Products",
["orders"] = "dbo.Orders",
["customers"] = "dbo.Customers"
};
public DataTable GetDataSafely(string tableKey)
{
if (!TableMap.TryGetValue(tableKey.ToLowerInvariant(), out string actualTableName))
{
throw new ArgumentException("Invalid table key");
}
string query = $"SELECT * FROM {actualTableName}";
// Execute query...
}Even stored procedures can be vulnerable if you build the call statement with string concatenation or don't use parameters.
public void UpdateUserEmail(int userId, string newEmail)
{
// Don't do this!
string query = $"EXEC UpdateEmail {userId}, '{newEmail}'";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
connection.Open();
command.ExecuteNonQuery();
}
}
// Attacker sends: newEmail = "fake@test.com'; DROP TABLE Users--"public void UpdateUserEmail(int userId, string newEmail)
{
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand("UpdateEmail", connection))
{
// Set command type to stored procedure
command.CommandType = CommandType.StoredProcedure;
// Add parameters
command.Parameters.AddWithValue("@UserId", userId);
command.Parameters.AddWithValue("@NewEmail", newEmail);
// Add output parameter if needed
var result = new SqlParameter("@Result", SqlDbType.Int)
{
Direction = ParameterDirection.Output
};
command.Parameters.Add(result);
connection.Open();
command.ExecuteNonQuery();
int resultValue = (int)result.Value;
// Check result...
}
}Entity Framework provides protection, but using raw SQL methods incorrectly reintroduces injection vulnerabilities.
public List<User> SearchUsers(string searchTerm)
{
// Vulnerable to SQL injection!
var users = _context.Users
.FromSqlRaw($"SELECT * FROM Users WHERE Username LIKE '%{searchTerm}%'")
.ToList();
return users;
}public List<User> SearchUsers(string searchTerm)
{
// Use parameterized raw SQL
var users = _context.Users
.FromSqlRaw(
"SELECT * FROM Users WHERE Username LIKE '%' + @searchTerm + '%'",
new SqlParameter("@searchTerm", searchTerm)
)
.ToList();
return users;
}
// Or better: Use string interpolation (EF Core 3.0+)
public List<User> SearchUsersInterpolated(string searchTerm)
{
// FormattableString is automatically parameterized
var users = _context.Users
.FromSqlInterpolated($"SELECT * FROM Users WHERE Username LIKE '%' + {searchTerm} + '%'")
.ToList();
return users;
}
// Best: Use LINQ when possible
public List<User> SearchUsersLinq(string searchTerm)
{
var users = _context.Users
.Where(u => u.Username.Contains(searchTerm))
.ToList();
return users;
}Building LINQ expressions from strings can lead to injection if not handled carefully.
public List<Product> FilterProducts(string filterExpression)
{
// Using Dynamic LINQ unsafely
var products = _context.Products
.Where(filterExpression) // Dangerous!
.ToList();
return products;
}
// Attacker could inject: "true || Price = 0"public List<Product> FilterProducts(string category, decimal? minPrice, decimal? maxPrice)
{
// Build query using safe LINQ methods
IQueryable<Product> query = _context.Products;
if (!string.IsNullOrEmpty(category))
{
query = query.Where(p => p.Category == category);
}
if (minPrice.HasValue)
{
query = query.Where(p => p.Price >= minPrice.Value);
}
if (maxPrice.HasValue)
{
query = query.Where(p => p.Price <= maxPrice.Value);
}
return query.ToList();
}
// If you must use Dynamic LINQ, validate heavily
public List<Product> FilterProductsSafely(string propertyName, string operatorName, object value)
{
// Whitelist allowed properties
var allowedProperties = new HashSet<string> { "Category", "Price", "InStock" };
if (!allowedProperties.Contains(propertyName))
{
throw new ArgumentException("Invalid property");
}
// Whitelist allowed operators
var allowedOperators = new HashSet<string> { "==", ">", "<", ">=", "<=" };
if (!allowedOperators.Contains(operatorName))
{
throw new ArgumentException("Invalid operator");
}
// Build expression safely
var parameter = Expression.Parameter(typeof(Product), "p");
var property = Expression.Property(parameter, propertyName);
var constant = Expression.Constant(value);
Expression comparison = operatorName switch
{
"==" => Expression.Equal(property, constant),
">" => Expression.GreaterThan(property, constant),
"<" => Expression.LessThan(property, constant),
">=" => Expression.GreaterThanOrEqual(property, constant),
"<=" => Expression.LessThanOrEqual(property, constant),
_ => throw new ArgumentException("Invalid operator")
};
var lambda = Expression.Lambda<Func<Product, bool>>(comparison, parameter);
return _context.Products.Where(lambda).ToList();
}Dynamic ORDER BY clauses are a common injection point because column names can't be parameterized.
public List<Product> GetProductsSorted(string sortColumn, string sortDirection)
{
string query = $"SELECT * FROM Products ORDER BY {sortColumn} {sortDirection}";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
connection.Open();
// Execute...
}
}
// Attacker sends: sortColumn = "Price; DROP TABLE Products--"public List<Product> GetProductsSorted(string sortColumn, string sortDirection)
{
// Whitelist allowed columns
var allowedColumns = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["name"] = "Name",
["price"] = "Price",
["category"] = "Category",
["stock"] = "StockQuantity"
};
// Whitelist allowed directions
var allowedDirections = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"ASC", "DESC"
};
// Validate inputs
if (!allowedColumns.TryGetValue(sortColumn, out string actualColumn))
{
actualColumn = "Name"; // Default
}
if (!allowedDirections.Contains(sortDirection))
{
sortDirection = "ASC"; // Default
}
string query = $"SELECT * FROM Products ORDER BY {actualColumn} {sortDirection}";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
connection.Open();
// Execute...
}
}
// Entity Framework approach
public List<Product> GetProductsSortedEF(string sortColumn, bool ascending)
{
IQueryable<Product> query = _context.Products;
// Use switch expression for type-safe sorting
query = sortColumn.ToLowerInvariant() switch
{
"name" => ascending ? query.OrderBy(p => p.Name) : query.OrderByDescending(p => p.Name),
"price" => ascending ? query.OrderBy(p => p.Price) : query.OrderByDescending(p => p.Price),
"category" => ascending ? query.OrderBy(p => p.Category) : query.OrderByDescending(p => p.Category),
_ => query.OrderBy(p => p.Name) // Default
};
return query.ToList();
}Even with parameterized queries, LIKE clauses need special attention to prevent injection through wildcard characters.
public List<User> SearchUsernames(string searchTerm)
{
string query = "SELECT * FROM Users WHERE Username LIKE @SearchTerm";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
// Vulnerable: Wildcards in user input
command.Parameters.AddWithValue("@SearchTerm", "%" + searchTerm + "%");
connection.Open();
// Execute...
}
}
// Attacker sends: searchTerm = "%' OR '1'='1' --"
// While SQL injection is prevented, attacker can still use wildcards maliciouslypublic List<User> SearchUsernames(string searchTerm)
{
// Escape special LIKE characters
string escapedTerm = EscapeLikeValue(searchTerm);
string query = @"SELECT * FROM Users
WHERE Username LIKE @SearchTerm ESCAPE '\'";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@SearchTerm", "%" + escapedTerm + "%");
connection.Open();
var users = new List<User>();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
users.Add(new User
{
Id = reader.GetInt32(0),
Username = reader.GetString(1)
});
}
}
return users;
}
}
private string EscapeLikeValue(string value)
{
if (string.IsNullOrEmpty(value)) return value;
return value
.Replace("\\", "\\\\") // Escape the escape character
.Replace("%", "\\%") // Escape wildcard
.Replace("_", "\\_") // Escape single-character wildcard
.Replace("[", "\\["); // Escape bracket
}
// Entity Framework approach with validation
public List<User> SearchUsernamesEF(string searchTerm)
{
// Validate input length to prevent DoS
if (searchTerm.Length > 50)
{
throw new ArgumentException("Search term too long");
}
// EF Core automatically escapes LIKE wildcards in the value
var users = _context.Users
.Where(u => EF.Functions.Like(u.Username, $"%{searchTerm}%"))
.ToList();
return users;
}Allowing multiple SQL statements in one query opens the door to stacked query injection.
public void ExecuteUserQuery(string query)
{
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
connection.Open();
command.ExecuteNonQuery();
}
}
// Attacker sends: "SELECT * FROM Users; DROP TABLE Users; --"public DataTable ExecuteUserQuery(string tableName)
{
// Never allow arbitrary queries from users
// Instead, provide specific, safe operations
var allowedTables = new Dictionary<string, string>
{
["users"] = "Users",
["products"] = "Products",
["orders"] = "Orders"
};
if (!allowedTables.TryGetValue(tableName.ToLowerInvariant(), out string actualTable))
{
throw new ArgumentException("Invalid table name");
}
// Only allow safe, predefined operations
string query = $"SELECT * FROM {actualTable}";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
connection.Open();
var adapter = new SqlDataAdapter(command);
var dataTable = new DataTable();
adapter.Fill(dataTable);
return dataTable;
}
}
// If you must allow complex queries, use a query builder with strict validation
public class SafeQueryBuilder
{
private readonly HashSet<string> _allowedTables;
private readonly HashSet<string> _allowedColumns;
private string _tableName;
private List<string> _selectedColumns = new List<string>();
private List<(string column, object value)> _whereConditions = new List<(string, object)>();
public SafeQueryBuilder(HashSet<string> allowedTables, HashSet<string> allowedColumns)
{
_allowedTables = allowedTables;
_allowedColumns = allowedColumns;
}
public SafeQueryBuilder From(string tableName)
{
if (!_allowedTables.Contains(tableName))
throw new ArgumentException("Table not allowed");
_tableName = tableName;
return this;
}
public SafeQueryBuilder Select(params string[] columns)
{
foreach (var col in columns)
{
if (!_allowedColumns.Contains(col))
throw new ArgumentException($"Column not allowed: {col}");
_selectedColumns.Add(col);
}
return this;
}
public SafeQueryBuilder Where(string column, object value)
{
if (!_allowedColumns.Contains(column))
throw new ArgumentException("Column not allowed");
_whereConditions.Add((column, value));
return this;
}
public (string query, SqlParameter[] parameters) Build()
{
if (string.IsNullOrEmpty(_tableName))
throw new InvalidOperationException("Table not specified");
var selectClause = _selectedColumns.Any()
? string.Join(", ", _selectedColumns)
: "*";
var query = $"SELECT {selectClause} FROM {_tableName}";
var parameters = new List<SqlParameter>();
if (_whereConditions.Any())
{
var whereClauses = new List<string>();
for (int i = 0; i < _whereConditions.Count; i++)
{
var (column, value) = _whereConditions[i];
var paramName = $"@p{i}";
whereClauses.Add($"{column} = {paramName}");
parameters.Add(new SqlParameter(paramName, value));
}
query += " WHERE " + string.Join(" AND ", whereClauses);
}
return (query, parameters.ToArray());
}
}Data that was previously stored in the database might contain malicious SQL if it wasn't properly sanitized on input and is later used in queries without parameters.
// First request: Store malicious data
public void CreateUser(string username, string bio)
{
string query = "INSERT INTO Users (Username, Bio) VALUES (@Username, @Bio)";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Username", username);
command.Parameters.AddWithValue("@Bio", bio); // Stored safely
connection.Open();
command.ExecuteNonQuery();
}
}
// Second request: Use stored data unsafely
public List<Post> GetUserPosts(string username)
{
// Get user bio from database
string userBio = GetUserBio(username);
// Vulnerable: Using database content directly in query!
string query = $"SELECT * FROM Posts WHERE Tags LIKE '%{userBio}%'";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
connection.Open();
// Execute...
}
}
// Attacker sets bio to: "'; DROP TABLE Posts; --"public void CreateUser(string username, string bio)
{
// Validate and sanitize input before storage
if (bio.Length > 500)
throw new ArgumentException("Bio too long");
// Remove potentially dangerous characters if needed
bio = SanitizeBio(bio);
string query = "INSERT INTO Users (Username, Bio) VALUES (@Username, @Bio)";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Username", username);
command.Parameters.AddWithValue("@Bio", bio);
connection.Open();
command.ExecuteNonQuery();
}
}
public List<Post> GetUserPosts(string username)
{
string userBio = GetUserBio(username);
// Always use parameters, even with database content!
string query = "SELECT * FROM Posts WHERE Tags LIKE @SearchPattern";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@SearchPattern", $"%{EscapeLikeValue(userBio)}%");
connection.Open();
var posts = new List<Post>();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
posts.Add(new Post
{
Id = reader.GetInt32(0),
Title = reader.GetString(1),
Content = reader.GetString(2)
});
}
}
return posts;
}
}
private string SanitizeBio(string bio)
{
// Remove or encode potentially dangerous characters
// This is defense in depth - parameters are still required!
return bio.Replace("'", "''")
.Replace(";", "")
.Replace("--", "")
.Replace("/*", "")
.Replace("*/", "")
.Replace("xp_", "")
.Replace("sp_", "");
}- Use parameterized queries for all user input
- Validate and sanitize all input at entry points
- Whitelist table and column names if they must be dynamic
- Use stored procedures with proper parameter handling
- Prefer ORM LINQ queries over raw SQL when possible
- Escape LIKE wildcards when using pattern matching
- Use least privilege database accounts
- Log all database errors for security monitoring
- Use prepared statements consistently
- Review all raw SQL in code reviews
- Never concatenate strings to build SQL queries
- Never trust data from any source (even your database)
- Never use dynamic SQL unless absolutely necessary
- Never allow arbitrary table/column names without whitelisting
- Never execute multiple statements from user input
- Never use
AddWithValue()without type validation - Never assume stored procedures are automatically safe
- Never ignore SQL injection in "internal" tools
- Never use string interpolation for SQL queries
- Never skip input validation because "it's parameterized"
- Use automated scanners (SQLMap, Burp Suite)
- Test with:
' OR '1'='1,1'; DROP TABLE--,admin'-- - Test all input fields, even hidden ones
- Check API endpoints, not just web forms
- Test ORDER BY, GROUP BY, and HAVING clauses
- Verify error messages don't expose schema information