SQL injection via string-interpolated SQL passed to sqflite raw* / execute
ID |
dart.code.sql_injection |
Severity |
critical |
Remediation Complexity |
hard |
Remediation Risk |
high |
Remediation Effort |
medium |
Resource |
Code |
Language |
Dart |
Description
A sqflite raw query or execute call is built by concatenating attacker-controllable values into the SQL plane ($-interpolation or + concatenation). An attacker can inject arbitrary SQL to read, modify, or destroy database contents.
Rationale
sqflite exposes rawQuery, rawInsert, rawUpdate, rawDelete, and
execute, all of which accept a raw SQL string. When that string is
produced by Dart string interpolation ("SELECT * FROM users WHERE id = $id")
or + concatenation, every character of the interpolated value
crosses the language boundary into the SQL parser. The SQL engine
cannot tell metadata from data — a value containing ' OR 1=1 --,
'; DROP TABLE …, or a UNION SELECT clause changes the statement
semantically.
On a mobile device the database is local, so the immediate impact is
confidentiality and integrity of the user’s data: an attacker who
reaches this call (via deeplink parameter, file import, IPC, sync
payload, or a tampered WebView) can read sensitive rows, alter
balances or flags, escalate privileges by flipping is_admin-style
booleans, or destroy the database with a DROP/DELETE clause.
Combined with sqflite_sqlcipher decryption, SQL injection also
leaks data the user expected to be encrypted at rest.
The following code illustrates a vulnerable pattern detected by this rule:
class SqlInjectionSamples {
Future<void> rawQueryInterp(Database db, String userId) async {
// VULNERABLE: SQL injection via string-interpolated SQL passed to sqflite raw* / execute
final rows = await db.rawQuery('SELECT * FROM users WHERE id = $userId');
print('$rows');
}
}
Remediation
Use parameterized queries: keep the SQL string a literal with ?
placeholders, and pass the runtime values in the arguments (a.k.a.
whereArgs) named argument. sqflite escapes each value safely. For
INSERT/UPDATE/DELETE, prefer the higher-level insert,
update, delete helpers which take a column-to-value map.
// Vulnerable: tainted value concatenated into the SQL plane
final rows = await db.rawQuery(
"SELECT * FROM users WHERE id = $userId");
// Fixed: SQL is a literal, value goes through whereArgs
final rows = await db.rawQuery(
'SELECT * FROM users WHERE id = ?',
[userId],
);
// Fixed: higher-level helper escapes value automatically
await db.update(
'users',
{'name': name},
where: 'id = ?',
whereArgs: [id],
);
References
-
OWASP Top 10 2025 - A05 : Injection.
-
OWASP SQL Injection Prevention Cheat Sheet — Canonical guidance on parameterization, ORMs, allow-listing, and stored procedures
-
sqflite (pub.dev) — Flutter SQLite plugin — see
whereArgs/argumentsfor the safe API -
Talsec — OWASP Mobile Top 10 for Flutter M4 — Insufficient input/output validation in Flutter and Dart, with concrete SQL-i scenarios