Scala JDBC SQL Injection Vulnerability

Critical Risk SQL Injection
scalasql-injectionjdbcdatabaseinjectionstring-interpolationuser-inputprepared-statementsparameterized-queries

What it is

A critical vulnerability that occurs when Scala applications using JDBC construct SQL queries by concatenating or interpolating user input directly into query strings. This allows attackers to inject malicious SQL code through Scala's string interpolation features or concatenation operations, potentially leading to data theft, database corruption, or unauthorized access.

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

Using Scala's string interpolation (s-strings) to embed user input directly into SQL queries creates injection vulnerabilities. The s"" syntax performs direct string substitution without any escaping or validation, making it trivial for attackers to inject malicious SQL code.

Root causes

Scala String Interpolation in SQL Queries

Using Scala's string interpolation (s-strings) to embed user input directly into SQL queries creates injection vulnerabilities. The s"" syntax performs direct string substitution without any escaping or validation, making it trivial for attackers to inject malicious SQL code.

Preview example – SCALA
// 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)
  // Malicious input: "1 OR 1=1 --"

String Concatenation with + Operator

Using string concatenation with the + operator to build SQL queries with user input bypasses all security mechanisms and allows direct injection of malicious SQL code. This is one of the most common and dangerous patterns in Scala applications.

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

Dynamic ORDER BY and Field Names

Allowing user input to control ORDER BY clauses or field names in Scala applications creates injection vulnerabilities. Attackers can inject additional SQL statements or subqueries through these dynamic components.

Preview example – SCALA
// 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)
  // Malicious input: sortColumn = "name; DROP TABLE users; --"

Complex Query Building with Maps and Collections

Building SQL queries dynamically based on Scala collections and maps without proper parameterization creates multiple injection points. This pattern is common in Scala applications that need flexible query building capabilities.

Preview example – SCALA
// 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"
  // Each filter value is a potential injection point

Fixes

1

Use PreparedStatement with Parameter Binding

Always use JDBC PreparedStatement with parameter binding instead of string interpolation or concatenation. This separates SQL logic from data and ensures user input is treated as data rather than executable code.

View implementation – SCALA
// 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
}
2

Implement Type-Safe Input Validation

Use Scala's type system and validation libraries to ensure input is properly validated before use in database queries. Implement comprehensive validation functions that leverage Scala's functional programming features.

View implementation – SCALA
// Type-safe input validation
sealed trait ValidationError
case class InvalidUserID(value: String) extends ValidationError
case class InvalidEmail(value: String) extends ValidationError
case class InvalidDepartment(value: String) extends ValidationError

def validateUserID(userID: String): Either[ValidationError, Int] = {
  Try(userID.toInt) match {
    case Success(id) if id > 0 => Right(id)
    case _ => Left(InvalidUserID(userID))
  }
}

def validateEmail(email: String): Either[ValidationError, String] = {
  if (email.nonEmpty && email.contains("@") && email.length <= 100) {
    Right(email.trim.toLowerCase)
  } else {
    Left(InvalidEmail(email))
  }
}

def validateDepartment(dept: String): Either[ValidationError, String] = {
  val validDepartments = Set("IT", "HR", "Finance", "Marketing", "Sales")
  if (validDepartments.contains(dept)) {
    Right(dept)
  } else {
    Left(InvalidDepartment(dept))
  }
}
3

Use Whitelisting for Dynamic Field Names

When building dynamic queries in Scala, use whitelisted field names and operations. Create type-safe mappings of user-friendly names to actual column names to prevent injection through field manipulation.

View implementation – SCALA
// SECURE: Whitelisted field mapping
sealed trait SortField
case object NameSort extends SortField
case object EmailSort extends SortField
case object DepartmentSort extends SortField
case object CreatedSort extends SortField

sealed trait SortDirection
case object Ascending extends SortDirection
case object Descending extends SortDirection

object SortField {
  def fromString(field: String): Option[SortField] = field.toLowerCase match {
    case "name" => Some(NameSort)
    case "email" => Some(EmailSort)
    case "department" => Some(DepartmentSort)
    case "created" => Some(CreatedSort)
    case _ => None
  }
  
  def toColumnName(field: SortField): String = field match {
    case NameSort => "name"
    case EmailSort => "email"
    case DepartmentSort => "department"
    case CreatedSort => "created_at"
  }
}

def getUsersSorted(sortField: String, direction: String): Either[String, List[User]] = {
  for {
    field <- SortField.fromString(sortField).toRight(s"Invalid sort field: $sortField")
    dir <- parseSortDirection(direction)
  } yield {
    val columnName = SortField.toColumnName(field)
    val directionStr = if (dir == Ascending) "ASC" else "DESC"
    
    // Safe because columnName and directionStr are validated
    val query = s"SELECT id, name, email, department FROM users ORDER BY $columnName $directionStr LIMIT 100"
    executeQuery(query)
  }
}
4

Build Dynamic Queries Safely with Parameterization

When building complex dynamic queries in Scala, separate the construction of SQL structure from user data insertion. Use parameterized conditions and build queries functionally.

View implementation – SCALA
// SECURE: Safe dynamic query building
case class SearchCriteria(name: Option[String], department: Option[String], status: Option[String])

def searchUsers(criteria: SearchCriteria): Either[ValidationError, List[User]] = {
  // Validate all criteria first
  val validatedCriteria = for {
    name <- criteria.name.traverse(validateSearchTerm)
    dept <- criteria.department.traverse(validateDepartment)
    status <- criteria.status.traverse(validateStatus)
  } yield SearchCriteria(name, dept, status)
  
  validatedCriteria.map { validated =>
    val conditions = List.newBuilder[String]
    val params = List.newBuilder[Any]
    
    var baseQuery = "SELECT id, name, email, department FROM users WHERE 1=1"
    
    validated.name.foreach { name =>
      conditions += " AND name ILIKE ?"
      params += s"%$name%"
    }
    
    validated.department.foreach { dept =>
      conditions += " AND department = ?"
      params += dept
    }
    
    validated.status.foreach { status =>
      conditions += " AND status = ?"
      params += status
    }
    
    val query = baseQuery + conditions.result().mkString
    val paramList = params.result()
    
    Using(connection.prepareStatement(query)) { pstmt =>
      paramList.zipWithIndex.foreach { case (param, index) =>
        pstmt.setObject(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
    }.get
  }
}
5

Use Functional Error Handling and Resource Management

Leverage Scala's functional programming features like Try, Either, and Using for proper error handling and resource management. This ensures database connections are properly closed and errors are handled gracefully.

View implementation – SCALA
// SECURE: Functional error handling and resource management
import scala.util.{Try, Using}
import scala.util.control.NonFatal

def updateUserSafely(userEmail: String, newSalaryStr: String): Either[String, Int] = {
  val result = for {
    email <- validateEmail(userEmail)
    salary <- validateSalary(newSalaryStr)
  } yield {
    Try {
      Using(connection.prepareStatement("UPDATE users SET salary = ? WHERE email = ?")) { pstmt =>
        pstmt.setInt(1, salary)
        pstmt.setString(2, email)
        pstmt.executeUpdate()
      }.get
    }.recover {
      case NonFatal(ex) =>
        // Log error without exposing internal details
        logger.error(s"Database update failed for user $email", ex)
        throw new RuntimeException("Database operation failed")
    }.get
  }
  
  result.map(Right(_)).getOrElse(Left("Validation failed"))
}

// Transaction support with functional style
def batchUpdateWithTransaction(updates: List[(String, Int)]): Either[String, Int] = {
  Try {
    connection.setAutoCommit(false)
    
    val totalUpdated = Using(connection.prepareStatement("UPDATE users SET salary = ? WHERE email = ?")) { pstmt =>
      updates.foldLeft(0) { case (count, (email, salary)) =>
        validateEmail(email).flatMap(_ => validateSalary(salary.toString)) match {
          case Right(_) =>
            pstmt.setInt(1, salary)
            pstmt.setString(2, email)
            pstmt.addBatch()
            count + 1
          case Left(error) =>
            throw new IllegalArgumentException(s"Validation failed: $error")
        }
      }
      
      val results = pstmt.executeBatch()
      connection.commit()
      results.sum
    }.get
    
    Right(totalUpdated)
  }.recover {
    case NonFatal(ex) =>
      Try(connection.rollback())
      Left(s"Transaction failed: ${ex.getMessage}")
  }.get.andThen { _ =>
    Try(connection.setAutoCommit(true))
  }
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies scala jdbc sql injection vulnerability and many other security issues in your codebase.