SQL injection from user data in manually constructed SQL string in JDBC code

Critical Risk SQL Injection
scalasql-injectionjdbcdatabasejvm

What it is

SQL injection vulnerability in Scala applications where untrusted input is concatenated or interpolated into SQL strings instead of using parameterized queries or prepared statements, allowing attackers to manipulate database operations.

import java.sql.{Connection, DriverManager, ResultSet, Statement}
import scala.util.{Try, Success, Failure}

case class User(id: Int, name: String, email: String, department: String)

class UserRepository(connection: Connection) {

  // VULNERABLE: String interpolation
  def getUserById(userId: String): Option[User] = {
    val query = s"SELECT * FROM users WHERE id = $userId"

    val stmt = connection.createStatement()
    val rs = stmt.executeQuery(query)

    if (rs.next()) {
      Some(User(rs.getInt("id"), rs.getString("name"),
               rs.getString("email"), rs.getString("department")))
    } else {
      None
    }
  }

  // VULNERABLE: String concatenation
  def searchUsers(name: String, department: String): List[User] = {
    val query = "SELECT * FROM users WHERE name LIKE '%" + name +
                "%' AND department = '" + department + "'"

    val stmt = connection.createStatement()
    val rs = stmt.executeQuery(query)

    val users = scala.collection.mutable.ListBuffer[User]()
    while (rs.next()) {
      users += User(rs.getInt("id"), rs.getString("name"),
                   rs.getString("email"), rs.getString("department"))
    }
    users.toList
  }

  // VULNERABLE: String interpolation in updates
  def updateUserStatus(userEmail: String, newStatus: String): Boolean = {
    val query = s"UPDATE users SET status = '$newStatus' WHERE email = '$userEmail'"

    val stmt = connection.createStatement()
    val rowsAffected = stmt.executeUpdate(query)
    rowsAffected > 0
  }

  // VULNERABLE: Dynamic ORDER BY
  def getUsersSorted(sortColumn: String, direction: String): List[User] = {
    val query = s"SELECT * FROM users ORDER BY $sortColumn $direction"

    val stmt = connection.createStatement()
    val rs = stmt.executeQuery(query)

    val users = scala.collection.mutable.ListBuffer[User]()
    while (rs.next()) {
      users += User(rs.getInt("id"), rs.getString("name"),
                   rs.getString("email"), rs.getString("department"))
    }
    users.toList
  }

  // VULNERABLE: Complex query building
  def advancedSearch(filters: Map[String, String]): List[User] = {
    val conditions = filters.map { case (field, value) =>
      s"$field = '$value'"
    }.mkString(" AND ")

    val query = s"SELECT * FROM users WHERE $conditions"

    val stmt = connection.createStatement()
    val rs = stmt.executeQuery(query)

    val users = scala.collection.mutable.ListBuffer[User]()
    while (rs.next()) {
      users += User(rs.getInt("id"), rs.getString("name"),
                   rs.getString("email"), rs.getString("department"))
    }
    users.toList
  }
}

// Usage with malicious inputs:
// getUserById("1 OR 1=1 --")
// searchUsers("'; DROP TABLE users; --", "IT")
// updateUserStatus("user@example.com", "'; DELETE FROM users; --")
// getUsersSorted("name; DROP TABLE users; --", "ASC")
import java.sql.{Connection, DriverManager, PreparedStatement, ResultSet}
import scala.util.{Try, Success, Failure, Using}
import scala.collection.mutable.ListBuffer

case class User(id: Int, name: String, email: String, department: String)

class UserRepository(connection: Connection) {

  private def validateUserId(userId: String): Int = {
    Try(userId.toInt) match {
      case Success(id) if id > 0 => id
      case _ => throw new IllegalArgumentException(s"Invalid user ID: $userId")
    }
  }

  private def validateEmail(email: String): String = {
    require(email.nonEmpty && email.contains("@") && email.length <= 100,
            "Invalid email format")
    email.trim
  }

  // SECURE: PreparedStatement with parameter binding
  def getUserById(userIdStr: String): Option[User] = {
    val userId = validateUserId(userIdStr)
    val query = "SELECT id, name, email, department FROM users WHERE id = ?"

    Using(connection.prepareStatement(query)) { pstmt =>
      pstmt.setInt(1, userId)
      val rs = pstmt.executeQuery()

      if (rs.next()) {
        Some(User(rs.getInt("id"), rs.getString("name"),
                 rs.getString("email"), rs.getString("department")))
      } else {
        None
      }
    }.recover {
      case ex => throw new RuntimeException(s"Database error: ${ex.getMessage}", ex)
    }.get
  }

  // SECURE: Parameterized query with validation
  def searchUsers(name: String, department: String): List[User] = {
    require(name.length <= 100, "Search name too long")

    val validDepartments = Set("IT", "HR", "Finance", "Marketing", "Sales")
    require(department.isEmpty || validDepartments.contains(department),
            s"Invalid department: $department")

    val conditions = ListBuffer[String]()
    val params = ListBuffer[String]()

    var query = "SELECT id, name, email, department FROM users WHERE 1=1"

    if (name.nonEmpty) {
      conditions += " AND name ILIKE ?"
      params += s"%$name%"
    }

    if (department.nonEmpty) {
      conditions += " AND department = ?"
      params += department
    }

    query += conditions.mkString + " LIMIT 100"

    Using(connection.prepareStatement(query)) { pstmt =>
      params.zipWithIndex.foreach { case (param, index) =>
        pstmt.setString(index + 1, param)
      }

      val rs = pstmt.executeQuery()
      val users = ListBuffer[User]()

      while (rs.next()) {
        users += User(rs.getInt("id"), rs.getString("name"),
                     rs.getString("email"), rs.getString("department"))
      }
      users.toList
    }.recover {
      case ex => throw new RuntimeException(s"Database error: ${ex.getMessage}", ex)
    }.get
  }

  // SECURE: Parameterized update with validation
  def updateUserStatus(userEmail: String, newStatus: String): Boolean = {
    val email = validateEmail(userEmail)

    val validStatuses = Set("active", "inactive", "suspended", "pending")
    require(validStatuses.contains(newStatus), s"Invalid status: $newStatus")

    val query = "UPDATE users SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE email = ?"

    Using(connection.prepareStatement(query)) { pstmt =>
      pstmt.setString(1, newStatus)
      pstmt.setString(2, email)

      val rowsAffected = pstmt.executeUpdate()
      rowsAffected > 0
    }.recover {
      case ex => throw new RuntimeException(s"Database error: ${ex.getMessage}", ex)
    }.get
  }

  // SECURE: Whitelisted sort columns
  def getUsersSorted(sortColumn: String, direction: String): List[User] = {
    val validSortColumns = Map(
      "name" -> "name",
      "email" -> "email",
      "department" -> "department",
      "created" -> "created_at"
    )

    val validDirections = Set("ASC", "DESC")

    val column = validSortColumns.getOrElse(sortColumn,
                   throw new IllegalArgumentException(s"Invalid sort column: $sortColumn"))

    require(validDirections.contains(direction.toUpperCase),
            s"Invalid sort direction: $direction")

    // Safe because column and direction are whitelisted
    val query = s"SELECT id, name, email, department FROM users ORDER BY $column ${direction.toUpperCase} LIMIT 100"

    Using(connection.prepareStatement(query)) { pstmt =>
      val rs = pstmt.executeQuery()
      val users = ListBuffer[User]()

      while (rs.next()) {
        users += User(rs.getInt("id"), rs.getString("name"),
                     rs.getString("email"), rs.getString("department"))
      }
      users.toList
    }.recover {
      case ex => throw new RuntimeException(s"Database error: ${ex.getMessage}", ex)
    }.get
  }

  // SECURE: Whitelisted fields with parameterized values
  def advancedSearch(filters: Map[String, String]): List[User] = {
    val allowedFields = Set("name", "department", "status", "email")

    // Validate all field names
    filters.keys.foreach { field =>
      require(allowedFields.contains(field), s"Invalid search field: $field")
    }

    val conditions = ListBuffer[String]()
    val params = ListBuffer[String]()

    var query = "SELECT id, name, email, department FROM users WHERE 1=1"

    filters.foreach { case (field, value) =>
      conditions += s" AND $field = ?"  // Safe because field is whitelisted
      params += value
    }

    query += conditions.mkString + " LIMIT 100"

    Using(connection.prepareStatement(query)) { pstmt =>
      params.zipWithIndex.foreach { case (param, index) =>
        pstmt.setString(index + 1, param)
      }

      val rs = pstmt.executeQuery()
      val users = ListBuffer[User]()

      while (rs.next()) {
        users += User(rs.getInt("id"), rs.getString("name"),
                     rs.getString("email"), rs.getString("department"))
      }
      users.toList
    }.recover {
      case ex => throw new RuntimeException(s"Database error: ${ex.getMessage}", ex)
    }.get
  }

  // Example with transaction support
  def batchUpdateStatuses(updates: List[(String, String)]): Try[Int] = {
    Try {
      connection.setAutoCommit(false)

      val query = "UPDATE users SET status = ? WHERE email = ?"
      Using(connection.prepareStatement(query)) { pstmt =>
        var totalUpdated = 0

        updates.foreach { case (email, status) =>
          val validatedEmail = validateEmail(email)

          val validStatuses = Set("active", "inactive", "suspended", "pending")
          require(validStatuses.contains(status), s"Invalid status: $status")

          pstmt.setString(1, status)
          pstmt.setString(2, validatedEmail)
          pstmt.addBatch()
        }

        val results = pstmt.executeBatch()
        connection.commit()

        results.sum
      }.get
    }.recover {
      case ex =>
        connection.rollback()
        throw ex
    }.andThen { _ =>
      connection.setAutoCommit(true)
    }
  }
}

💡 Why This Fix Works

The vulnerable code uses string interpolation and concatenation to build SQL queries with user input, allowing injection attacks. The fixed version uses PreparedStatement with ? placeholders, implements comprehensive input validation, whitelisting for dynamic fields, proper error handling with Try/Using, and transaction support for batch operations.

Why it happens

Untrusted input is concatenated or interpolated into SQL strings instead of using parameterized queries or prepared statements.

Root causes

String Interpolation in SQL Queries

Untrusted input is concatenated or interpolated into SQL strings instead of using parameterized queries or prepared statements.

Preview example – SCALA
// VULNERABLE: String interpolation
def getUserById(userId: String): Option[User] = {
  val query = s"SELECT * FROM users WHERE id = $userId"
  // String interpolation allows SQL injection
  executeQuery(query)
}

Manual SQL Construction with User Input

Building SQL queries by concatenating user input directly into query strings bypasses database parameter binding mechanisms.

Preview example – SCALA
// VULNERABLE: String concatenation
def searchUsers(name: String, department: String): List[User] = {
  val query = "SELECT * FROM users WHERE name = '" + name +
              "' AND department = '" + department + "'"
  // Allows injection: name = "'; DROP TABLE users; --"
  executeQuery(query)
}

Fixes

1

Use JDBC PreparedStatement with Parameter Binding

Replace string concatenation or interpolation with parameterized queries using JDBC PreparedStatement or your ORM. Use placeholders and bind values as parameters.

View implementation – SCALA
// SECURE: PreparedStatement with parameters
def getUserById(userId: String): Option[User] = {
  val query = "SELECT * FROM users WHERE id = ?"
  val pstmt = connection.prepareStatement(query)
  pstmt.setInt(1, userId.toInt)
  val rs = pstmt.executeQuery()
  processResult(rs)
}
2

Validate and Whitelist Dynamic Identifiers

Validate and type-check inputs; avoid constructing table or column names from input. Use whitelisting for dynamic identifiers.

View implementation – SCALA
// SECURE: Input validation and whitelisting
def searchUsers(name: String, department: String, sortBy: String): List[User] = {
  // Validate inputs
  require(name.length <= 100, "Name too long")

  val validDepartments = Set("IT", "HR", "Finance", "Marketing")
  require(validDepartments.contains(department), "Invalid department")

  val validSortColumns = Map(
    "name" -> "name ASC",
    "email" -> "email ASC",
    "created" -> "created_at DESC"
  )
  val orderClause = validSortColumns.getOrElse(sortBy, "name ASC")

  val query = s"SELECT * FROM users WHERE name LIKE ? AND department = ? ORDER BY $orderClause"
  val pstmt = connection.prepareStatement(query)
  pstmt.setString(1, s"%$name%")
  pstmt.setString(2, department)
  val rs = pstmt.executeQuery()
  processResults(rs)
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies sql injection from user data in manually constructed sql string in jdbc code and many other security issues in your codebase.