What is SQL Injection?
SQL Injection (SQLi) is a code injection vulnerability that occurs when an application incorporates user-supplied data into SQL queries without proper sanitization or parameterization. An attacker can manipulate the SQL query logic to:
- Extract data — Read tables, columns, and records they shouldn’t have access to
- Modify data — Insert, update, or delete records
- Bypass authentication — Log in as any user without knowing passwords
- Execute commands — In some database configurations, run operating system commands on the server
SQLi consistently ranks among the most dangerous web application vulnerabilities. It’s listed in the OWASP Top 10 (A03:2021 Injection) and CWE Top 25 (CWE-89).
Why it matters
SQL Injection is not a theoretical risk. It has been the root cause of some of the largest data breaches in history, including breaches that exposed hundreds of millions of user records. Despite being well-understood for over two decades, SQLi remains prevalent because:
- Legacy code often uses string concatenation for queries
- Developers may not understand the difference between escaping and parameterization
- ORMs can be bypassed with raw query methods
- Second-order injection (stored input used in later queries) is hard to spot manually
How exploitation works
Basic example
Consider this C# code that builds a login query:
// VULNERABLE: User input directly concatenated into SQL
string query = "SELECT * FROM Users WHERE Username = '" + username + "' AND Password = '" + password + "'";
SqlCommand cmd = new SqlCommand(query, connection);
An attacker enters admin' -- as the username. The resulting query becomes:
SELECT * FROM Users WHERE Username = 'admin' --' AND Password = ''
The -- comments out the password check, granting access as admin without knowing the password.
Blind SQL Injection
When the application doesn’t return query results directly, attackers use boolean-based or time-based techniques:
// Boolean-based: different responses reveal data
/user?id=1 AND 1=1 → normal response
/user?id=1 AND 1=2 → different response
// Time-based: response timing reveals data
/user?id=1; WAITFOR DELAY '0:0:5'-- → 5-second delay confirms injection
Vulnerable code examples
C# / ASP.NET
// VULNERABLE
public User GetUser(string id)
{
string query = $"SELECT * FROM Users WHERE Id = {id}";
using var cmd = new SqlCommand(query, connection);
// attacker input: 1 OR 1=1
}
Java / JDBC
// VULNERABLE
public User findUser(String username) {
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
return jdbcTemplate.queryForObject(sql, userRowMapper);
}
Python / Django
# VULNERABLE — bypassing Django ORM with raw SQL
def get_user(request):
user_id = request.GET.get('id')
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
PHP
// VULNERABLE
$query = "SELECT * FROM users WHERE id = " . $_GET['id'];
$result = mysqli_query($conn, $query);
JavaScript / Node.js
// VULNERABLE (mysql2 / pg)
app.get('/user', async (req, res) => {
const { id } = req.query;
const rows = await db.query(`SELECT * FROM users WHERE id = ${id}`);
res.json(rows);
});
TypeScript / Node.js
// VULNERABLE
async function getUser(id: string): Promise<User> {
const result = await pool.query(`SELECT * FROM users WHERE id = ${id}`);
return result.rows[0];
}
Go
// VULNERABLE
func getUser(db *sql.DB, id string) (*User, error) {
query := "SELECT * FROM users WHERE id = " + id
row := db.QueryRow(query)
// ...
}
Ruby / Rails
# VULNERABLE — bypassing ActiveRecord safety
def find_user(params)
User.where("username = '#{params[:username]}'").first
end
Kotlin / Spring
// VULNERABLE
fun findUser(username: String): User? {
val sql = "SELECT * FROM users WHERE username = '$username'"
return jdbcTemplate.queryForObject(sql, userMapper)
}
Scala / Play
// VULNERABLE
def getUser(id: String) = Action {
val sql = s"SELECT * FROM users WHERE id = $id"
val result = DB.withConnection { implicit c => SQL(sql).as(userParser.single) }
Ok(Json.toJson(result))
}
Rust
// VULNERABLE (sqlx with raw query formatting)
async fn get_user(pool: &PgPool, id: &str) -> Result<User, sqlx::Error> {
let query = format!("SELECT * FROM users WHERE id = {}", id);
sqlx::query_as::<_, User>(&query).fetch_one(pool).await
}
Perl
# VULNERABLE
sub get_user {
my ($dbh, $id) = @_;
my $sth = $dbh->prepare("SELECT * FROM users WHERE id = $id");
$sth->execute();
}
VB.NET
' VULNERABLE
Public Function GetUser(id As String) As DataRow
Dim query As String = "SELECT * FROM Users WHERE Id = " & id
Dim cmd As New SqlCommand(query, connection)
' ...
End Function
Groovy / Grails
// VULNERABLE
def getUser(String username) {
def sql = "SELECT * FROM users WHERE username = '${username}'"
return Sql.newInstance(dataSource).rows(sql)
}
ColdFusion
<!--- VULNERABLE --->
<cfquery name="getUser" datasource="myDB">
SELECT * FROM users WHERE id = #url.id#
</cfquery>
C / C++
// VULNERABLE (libpq)
void get_user(PGconn *conn, const char *id) {
char query[512];
snprintf(query, sizeof(query), "SELECT * FROM users WHERE id = %s", id);
PGresult *res = PQexec(conn, query);
}
R
# VULNERABLE (DBI + RMySQL)
get_user <- function(con, id) {
query <- paste0("SELECT * FROM users WHERE id = ", id)
dbGetQuery(con, query)
}
Swift / Vapor
// VULNERABLE
func getUser(_ req: Request) async throws -> User {
let id = req.parameters.get("id") ?? ""
let query = "SELECT * FROM users WHERE id = \(id)"
return try await req.db.raw(SQLQueryString(query)).first(decoding: User.self).get()!
}
Dart / SQLite
// VULNERABLE
Future<Map<String, dynamic>?> getUser(Database db, String id) async {
final results = await db.rawQuery('SELECT * FROM users WHERE id = $id');
return results.isNotEmpty ? results.first : null;
}
Elixir / Ecto
# VULNERABLE — using raw fragments without parameterization
def get_user(username) do
Repo.one(from u in User,
where: fragment("username = '#{username}'"))
end
Lua
-- VULNERABLE (LuaSQL)
function get_user(conn, id)
local cursor = conn:execute("SELECT * FROM users WHERE id = " .. id)
return cursor:fetch({}, "a")
end
COBOL
* VULNERABLE — dynamic SQL via EXECUTE IMMEDIATE
MOVE FUNCTION CONCATENATE(
"SELECT * FROM USERS WHERE ID = '",
WS-USER-ID,
"'")
TO WS-SQL-STMT
EXEC SQL EXECUTE IMMEDIATE :WS-SQL-STMT END-EXEC
Secure code examples
C# / ASP.NET
// SECURE: Parameterized query
public User GetUser(string id)
{
string query = "SELECT * FROM Users WHERE Id = @Id";
using var cmd = new SqlCommand(query, connection);
cmd.Parameters.AddWithValue("@Id", id);
// id is bound as data, never interpreted as SQL
}
Java / JDBC
// SECURE: PreparedStatement with parameter binding
public User findUser(String username) {
String sql = "SELECT * FROM users WHERE username = ?";
return jdbcTemplate.queryForObject(sql, userRowMapper, username);
}
Python / Django
# SECURE: Parameterized query
def get_user(request):
user_id = request.GET.get('id')
cursor.execute("SELECT * FROM users WHERE id = %s", [user_id])
PHP
// SECURE: PDO prepared statement
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute(['id' => $_GET['id']]);
JavaScript / Node.js
// SECURE (mysql2 / pg)
app.get('/user', async (req, res) => {
const { id } = req.query;
const rows = await db.query('SELECT * FROM users WHERE id = $1', [id]);
res.json(rows);
});
TypeScript / Node.js
// SECURE
async function getUser(id: string): Promise<User> {
const result = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
return result.rows[0];
}
Go
// SECURE: parameterized with placeholder
func getUser(db *sql.DB, id string) (*User, error) {
row := db.QueryRow("SELECT * FROM users WHERE id = $1", id)
// id is passed separately, never concatenated
}
Ruby / Rails
# SECURE — ActiveRecord parameterized
def find_user(params)
User.where(username: params[:username]).first
# or: User.where("username = ?", params[:username]).first
end
Kotlin / Spring
// SECURE
fun findUser(username: String): User? {
val sql = "SELECT * FROM users WHERE username = ?"
return jdbcTemplate.queryForObject(sql, userMapper, username)
}
Scala / Play
// SECURE — Anorm parameterized query
def getUser(id: String) = Action {
DB.withConnection { implicit c =>
val user = SQL("SELECT * FROM users WHERE id = {id}")
.on("id" -> id).as(userParser.single)
Ok(Json.toJson(user))
}
}
Rust
// SECURE (sqlx bind parameters)
async fn get_user(pool: &PgPool, id: &str) -> Result<User, sqlx::Error> {
sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_one(pool)
.await
}
Perl
# SECURE: DBI placeholder
sub get_user {
my ($dbh, $id) = @_;
my $sth = $dbh->prepare("SELECT * FROM users WHERE id = ?");
$sth->execute($id);
}
VB.NET
' SECURE: SqlParameter
Public Function GetUser(id As String) As DataRow
Dim query As String = "SELECT * FROM Users WHERE Id = @Id"
Dim cmd As New SqlCommand(query, connection)
cmd.Parameters.AddWithValue("@Id", id)
' ...
End Function
Groovy / Grails
// SECURE — Grails ORM (GORM) with named params
def getUser(String username) {
return User.findByUsername(username)
// or: Sql.newInstance(dataSource).rows("SELECT * FROM users WHERE username = ?", [username])
}
ColdFusion
<!--- SECURE: cfqueryparam --->
<cfquery name="getUser" datasource="myDB">
SELECT * FROM users WHERE id = <cfqueryparam value="#url.id#" cfsqltype="cf_sql_integer">
</cfquery>
C / C++
// SECURE (libpq parameterized)
void get_user(PGconn *conn, const char *id) {
const char *params[1] = { id };
PGresult *res = PQexecParams(conn,
"SELECT * FROM users WHERE id = $1",
1, NULL, params, NULL, NULL, 0);
}
R
# SECURE (DBI parameterized)
get_user <- function(con, id) {
dbGetQuery(con, "SELECT * FROM users WHERE id = ?", params = list(id))
}
Swift / Vapor
// SECURE: Fluent ORM query
func getUser(_ req: Request) async throws -> User {
let id = try req.parameters.require("id", as: UUID.self)
guard let user = try await User.find(id, on: req.db) else {
throw Abort(.notFound)
}
return user
}
Dart / SQLite
// SECURE: positional parameters
Future<Map<String, dynamic>?> getUser(Database db, String id) async {
final results = await db.rawQuery('SELECT * FROM users WHERE id = ?', [id]);
return results.isNotEmpty ? results.first : null;
}
Elixir / Ecto
# SECURE — Ecto parameterized query
def get_user(username) do
Repo.get_by(User, username: username)
# or: Repo.one(from u in User, where: u.username == ^username)
end
Lua
-- SECURE (LuaSQL prepared statement)
function get_user(conn, id)
local stmt = conn:prepare("SELECT * FROM users WHERE id = ?")
stmt:bind(1, id)
local cursor = stmt:execute()
return cursor:fetch({}, "a")
end
COBOL
* SECURE — static SQL with host variables
MOVE WS-USER-ID TO :HV-USER-ID
EXEC SQL
SELECT NAME INTO :HV-NAME
FROM USERS
WHERE ID = :HV-USER-ID
END-EXEC
What Offensive360 detects
Our SAST engine traces data flow from user input sources (HTTP parameters, form data, headers, cookies) through your application code to SQL query construction. We detect:
- Direct concatenation — String interpolation or concatenation used in SQL queries
- Unsafe ORM usage — Raw query methods in Django, ActiveRecord, Entity Framework with unsanitized input
- Stored procedures — Dynamic SQL within stored procedures that use unsanitized parameters
- Second-order injection — Data stored from one request used unsafely in a later query
- Partial sanitization — Cases where escaping is used instead of parameterization
Each finding includes the complete data-flow trace from source to sink, making it clear exactly how user input reaches the vulnerable query.
Remediation guidance
-
Always use parameterized queries — This is the primary defense. Every major language and framework supports them.
-
Use your ORM correctly — ORMs like Entity Framework, Django ORM, and ActiveRecord protect against SQLi when used properly. Avoid raw query methods with user input.
-
Apply input validation — Validate that input matches expected formats (e.g., numeric IDs should be integers). This is defense-in-depth, not a primary fix.
-
Use least-privilege database accounts — The application’s database user should only have the permissions it needs. Don’t connect as
saorroot. -
Enable WAF rules — Web Application Firewalls can catch common SQLi patterns, but should not be your only defense.
False-positive considerations
Offensive360 may flag SQL query construction that is actually safe in certain cases:
- Constant strings — If the concatenated value is a compile-time constant (not user input), there is no injection risk
- Validated numeric input — If input is parsed to an integer before use, the risk is mitigated
- Allow-listed values — If the application checks input against a fixed set of allowed values
Our engine applies taint analysis to reduce these false positives, but some edge cases may require manual review.