Scala Slick SQL Override with Literal Vulnerability

Critical Risk SQL Injection
ScalaSlickSQL InjectionDatabaseString InterpolationSQL Override

What it is

Application uses Slick's sql interpolation or overrideSql with literal user input, creating SQL injection vulnerabilities that can compromise database security.

import slick.jdbc.H2Profile.api._ import scala.concurrent.Future class UserRepository(db: Database) { // Vulnerable: Direct string interpolation in SQL def findUserByName(name: String): Future[Seq[User]] = { val query = sql"SELECT * FROM users WHERE name = '$name'".as[User] db.run(query) } // Vulnerable: sqlu with user input def updateUserStatus(userId: Int, status: String): Future[Int] = { val updateQuery = sqlu"UPDATE users SET status = '$status' WHERE id = $userId" db.run(updateQuery) } // Vulnerable: Complex query building def searchUsers(criteria: Map[String, String]): Future[Seq[User]] = { val conditions = criteria.map { case (field, value) => s"$field LIKE '%$value%'" }.mkString(" AND ") val query = sql"SELECT * FROM users WHERE #$conditions".as[User] db.run(query) } // Extremely vulnerable: Direct SQL execution def executeRawQuery(sqlQuery: String): Future[Vector[String]] = { val query = sql"#$sqlQuery".as[String] db.run(query) } }
import slick.jdbc.H2Profile.api._ import scala.concurrent.Future import scala.util.{Try, Success, Failure} case class User(id: Int, name: String, email: String, status: String) class Users(tag: Tag) extends Table[User](tag, "users") { def id = column[Int]("id", O.PrimaryKey, O.AutoInc) def name = column[String]("name") def email = column[String]("email") def status = column[String]("status") def * = (id, name, email, status) <> (User.tupled, User.unapply) } class UserRepository(db: Database) { val users = TableQuery[Users] // Input validation helper def validateString(input: String, maxLength: Int = 100): Either[String, String] = { if (input == null || input.trim.isEmpty) { Left("Input cannot be empty") } else if (input.length > maxLength) { Left(s"Input too long (max $maxLength characters)") } else if (input.contains("'") || input.contains("\"") || input.contains(";")) { Left("Input contains invalid characters") } else { Right(input.trim) } } def validateUserId(id: String): Either[String, Int] = { Try(id.toInt) match { case Success(value) if value > 0 => Right(value) case _ => Left("Invalid user ID") } } // Secure: Use type-safe Slick queries def findUserByName(name: String): Future[Either[String, Seq[User]]] = { validateString(name, 50) match { case Right(validName) => val query = users.filter(_.name === validName) db.run(query.result).map(Right(_)) case Left(error) => Future.successful(Left(error)) } } // Secure: Use parameterized queries def findUserByNamePattern(namePattern: String): Future[Either[String, Seq[User]]] = { validateString(namePattern, 30) match { case Right(validPattern) => // Safe: Use # for parameter binding val query = sql"SELECT id, name, email, status FROM users WHERE name LIKE ${s"%$validPattern%"}".as[User] db.run(query).map(Right(_)) case Left(error) => Future.successful(Left(error)) } } // Secure: Type-safe update def updateUserStatus(userIdStr: String, status: String): Future[Either[String, Int]] = { val validStatuses = Set("active", "inactive", "pending", "suspended") for { userId <- Future.fromTry(Try(validateUserId(userIdStr)).flatten.toTry) validStatus <- Future.fromTry { if (validStatuses.contains(status)) Try(status) else Try(throw new IllegalArgumentException("Invalid status")) } } yield { val updateQuery = users.filter(_.id === userId).map(_.status).update(validStatus) db.run(updateQuery).map(Right(_)) } }.flatten.recover { case ex: IllegalArgumentException => Left(ex.getMessage) case _ => Left("Update failed") } // Secure: Safe search with type-safe queries def searchUsers(criteria: Map[String, String]): Future[Either[String, Seq[User]]] = { // Validate all criteria val validatedCriteria = criteria.map { case (field, value) => val allowedFields = Set("name", "email", "status") if (!allowedFields.contains(field)) { return Future.successful(Left(s"Field '$field' not allowed")) } validateString(value, 50) match { case Right(validValue) => (field, validValue) case Left(error) => return Future.successful(Left(s"Invalid value for $field: $error")) } } // Build type-safe query var query = users.filter(_ => true) // Start with all users validatedCriteria.foreach { case (field, value) => field match { case "name" => query = query.filter(_.name like s"%$value%") case "email" => query = query.filter(_.email like s"%$value%") case "status" => query = query.filter(_.status === value) } } db.run(query.result).map(Right(_)) } // Secure: No raw query execution def executeRawQuery(sqlQuery: String): Future[Either[String, String]] = { Future.successful(Left("Raw query execution is disabled for security")) } // Alternative: Safe complex queries using Slick API def findUsersWithComplexCriteria( nameOpt: Option[String], statusOpt: Option[String], limit: Int = 100 ): Future[Either[String, Seq[User]]] = { // Validate inputs val validLimit = math.min(limit, 1000) // Cap at 1000 var query = users nameOpt.foreach { name => validateString(name, 50) match { case Right(validName) => query = query.filter(_.name like s"%$validName%") case Left(error) => return Future.successful(Left(error)) } } statusOpt.foreach { status => val allowedStatuses = Set("active", "inactive", "pending", "suspended") if (allowedStatuses.contains(status)) { query = query.filter(_.status === status) } else { return Future.successful(Left("Invalid status")) } } val finalQuery = query.take(validLimit) db.run(finalQuery.result).map(Right(_)) } // Secure: Use prepared statements for dynamic queries def dynamicSearch(searchTerms: List[String]): Future[Either[String, Seq[User]]] = { if (searchTerms.isEmpty) { return Future.successful(Right(Seq.empty)) } // Validate all search terms val validatedTerms = searchTerms.take(5).map { term => validateString(term, 30) match { case Right(validTerm) => validTerm case Left(error) => return Future.successful(Left(error)) } } // Build safe query with parameters val conditions = validatedTerms.map { term => users.filter(u => u.name.like(s"%$term%") || u.email.like(s"%$term%")) } // Union all conditions val unionQuery = conditions.reduce(_ union _) db.run(unionQuery.result).map(Right(_)) } }

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Slick code uses overrideSql: query.overrideSql(s"SELECT * FROM users WHERE name = '$userName'"). overrideSql bypasses Slick's query builder. String interpolation with user input. SQL injection through userName variable. Defeats Slick's parameterization safety.

Root causes

Using overrideSql with String Interpolation

Slick code uses overrideSql: query.overrideSql(s"SELECT * FROM users WHERE name = '$userName'"). overrideSql bypasses Slick's query builder. String interpolation with user input. SQL injection through userName variable. Defeats Slick's parameterization safety.

Building Dynamic SQL with User-Controlled Fragments

Dynamic SQL: val condition = s"status = '$status'"; query.overrideSql(s"SELECT * FROM orders WHERE $condition"). User input in SQL strings. overrideSql accepts literal SQL. No parameterization. String building creates injection vectors.

Using overrideSql Instead of Slick Query API

Avoiding Slick syntax: overrideSql(rawSql) instead of query.filter(_.name === userName). Performance assumptions or complexity. Raw SQL more prone to errors. Slick query API provides safe parameterization. overrideSql circumvents protection.

Not Understanding overrideSql Security Implications

Developers unaware overrideSql disables safety. Believing Slick handles all escaping. Missing understanding of parameterization. overrideSql legacy method for compatibility. Modern Slick code shouldn't need overrideSql with user input.

Concatenating SQL Strings from Multiple Sources

Multi-source SQL: val sql = baseSql + whereClause + orderByClause; query.overrideSql(sql). whereClause or orderByClause contain user input. Complex string assembly. Even partial user control enables injection. String concatenation fundamentally unsafe for SQL.

Fixes

1

Use Slick Query API Instead of overrideSql

Type-safe queries: Users.filter(_.name === userName).result. Slick handles parameterization automatically. Compile-time type checking. Database-independent. No SQL injection risk. Prefer query API over raw SQL.

2

Use sql Interpolator with Bind Variables

Safe interpolation: sql"SELECT * FROM users WHERE name = $userName".as[User]. sql interpolator provides safe binding. $ creates bind variables. Type-safe result parsing. Parameterization handled by Slick. Correct way for raw SQL when needed.

3

Use sqlu for Updates with Parameters

Parameterized updates: sqlu"UPDATE users SET status = $newStatus WHERE id = $userId". sqlu interpolator for updates. Bind variables for all values. No string concatenation. Type-safe update operations. Prevents SQL injection in modifications.

4

Validate Dynamic Identifiers with Allowlists

If dynamic SQL needed: val allowedColumns = Set("name", "email", "status"); require(allowedColumns.contains(column)); sql"SELECT #$column FROM users".as[String]. # for identifiers. Validate against allowlist first. Can't parameterize column names. Allowlist essential.

5

Use Slick's Plain SQL API with Proper Parameters

Plain SQL safely: val query = sql"SELECT * FROM users WHERE active = $active AND role = $role".as[(String, String)]. Slick Plain SQL API. Typed parameters. Type-safe results. Safer than overrideSql. Use when query API insufficient.

6

Never Use overrideSql with User Input

Avoid overrideSql entirely with untrusted data. Search code: grep -r 'overrideSql' --include="*.scala". Review all usage. Replace with safe alternatives. overrideSql should only use constants. Code review rejecting overrideSql with variables.

Detect This Vulnerability in Your Code

Sourcery automatically identifies scala slick sql override with literal vulnerability and many other security issues in your codebase.