Skip to main content
Offensive360
Home / Knowledge Base / SQL Injection (SQLi)
Critical CWE-89 A03:2021 Injection

SQL Injection (SQLi)

SQL Injection occurs when untrusted input is concatenated into SQL queries, allowing attackers to read, modify, or delete database contents. Learn how to detect and prevent SQLi across languages.

Affects: C#JavaJavaScriptTypeScriptPythonPHPGoRubyKotlinScalaRustPerlVB.NETGroovyColdFusionC/C++RSwiftDartElixirLuaCOBOL

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:

  1. Legacy code often uses string concatenation for queries
  2. Developers may not understand the difference between escaping and parameterization
  3. ORMs can be bypassed with raw query methods
  4. 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

  1. Always use parameterized queries — This is the primary defense. Every major language and framework supports them.

  2. 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.

  3. 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.

  4. Use least-privilege database accounts — The application’s database user should only have the permissions it needs. Don’t connect as sa or root.

  5. 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.

References

By Offensive360 Security Research Reviewed: March 2026

Detect SQL Injection (SQLi) automatically

Run Offensive360 SAST on your codebase to find this and 100+ other vulnerabilities.