Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

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

SQL Injection (SQLi)

SQL injection concatenates untrusted input into queries, letting attackers read or modify database contents. Learn detection and prevention by language.

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.