Skip to content

Commit dbd5f14

Browse files
security(mysql): replace string concatenation with parameterized queries in schema introspection (#241) (#247)
1 parent d38ce73 commit dbd5f14

3 files changed

Lines changed: 100 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- **SQLite / RETURNING clause support (#243)** — support RETURNING clauses for INSERT, UPDATE, and DELETE DML queries in the SQLite database driver, returning the resulting rows to the client.
13+
- **Security / MySQL Injection Fix (#241)** — replaced manual escaping and string concatenation in schema introspection methods (`listViews`, `listColumnNames`, `listTables`) in the MySQL database driver with parameterized queries using parameter binding.
1314

1415
## [0.4.7-a] - 2026-06-22
1516

lib/core/database/mysql_connection.dart

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,7 @@ class MysqlConnection {
7676
bool get _usesConnectionString =>
7777
connectionString != null && connectionString!.trim().isNotEmpty;
7878

79-
static String _escapeSqlString(String s) {
80-
return s.replaceAll(r'\', r'\\').replaceAll("'", "''");
81-
}
79+
8280

8381
/// MySQL identifier quoting (backticks).
8482
static String quoteIdentifier(String id) {
@@ -284,11 +282,11 @@ class MysqlConnection {
284282
if (!isConnected || _conn == null) {
285283
throw StateError('Not connected to MySQL');
286284
}
287-
final s = _escapeSqlString(schema);
288285
final rs = await execute(
289286
'SELECT TABLE_NAME FROM information_schema.TABLES '
290-
"WHERE TABLE_SCHEMA = '$s' AND TABLE_TYPE = 'VIEW' "
287+
"WHERE TABLE_SCHEMA = :schema AND TABLE_TYPE = 'VIEW' "
291288
'ORDER BY TABLE_NAME',
289+
{'schema': schema},
292290
);
293291
return rs.rows.map((r) => r.colAt(0)!).toList();
294292
}
@@ -301,12 +299,14 @@ class MysqlConnection {
301299
if (!isConnected || _conn == null) {
302300
throw StateError('Not connected to MySQL');
303301
}
304-
final d = _escapeSqlString(database);
305-
final t = _escapeSqlString(table);
306302
final rs = await execute(
307303
'SELECT COLUMN_NAME FROM information_schema.COLUMNS '
308-
"WHERE TABLE_SCHEMA = '$d' AND TABLE_NAME = '$t' "
304+
"WHERE TABLE_SCHEMA = :database AND TABLE_NAME = :table "
309305
'ORDER BY ORDINAL_POSITION',
306+
{
307+
'database': database,
308+
'table': table,
309+
},
310310
);
311311
return rs.rows.map((r) => r.colAt(0)!).toList();
312312
}
@@ -316,11 +316,11 @@ class MysqlConnection {
316316
if (!isConnected || _conn == null) {
317317
throw StateError('Not connected to MySQL');
318318
}
319-
final s = _escapeSqlString(schema);
320319
final rs = await execute(
321320
'SELECT TABLE_NAME FROM information_schema.TABLES '
322-
"WHERE TABLE_SCHEMA = '$s' AND TABLE_TYPE = 'BASE TABLE' "
321+
"WHERE TABLE_SCHEMA = :schema AND TABLE_TYPE = 'BASE TABLE' "
323322
'ORDER BY TABLE_NAME',
323+
{'schema': schema},
324324
);
325325
return rs.rows.map((r) => r.colAt(0)!).toList();
326326
}

test/core/database/mysql_connection_test.dart

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,93 @@ void main() {
3939
);
4040
});
4141
});
42+
43+
group('MysqlConnection when not connected', () {
44+
late MysqlConnection conn;
45+
46+
setUp(() {
47+
conn = MysqlConnection(
48+
id: 1,
49+
name: 'test',
50+
host: 'localhost',
51+
);
52+
});
53+
54+
test('execute throws StateError', () {
55+
expect(
56+
() => conn.execute('SELECT 1'),
57+
throwsA(isA<StateError>().having(
58+
(e) => e.message,
59+
'message',
60+
contains('Not connected to MySQL'),
61+
)),
62+
);
63+
});
64+
65+
test('listDatabases throws StateError', () {
66+
expect(
67+
() => conn.listDatabases(),
68+
throwsA(isA<StateError>().having(
69+
(e) => e.message,
70+
'message',
71+
contains('Not connected to MySQL'),
72+
)),
73+
);
74+
});
75+
76+
test('listViews throws StateError', () {
77+
expect(
78+
() => conn.listViews(schema: 'db'),
79+
throwsA(isA<StateError>().having(
80+
(e) => e.message,
81+
'message',
82+
contains('Not connected to MySQL'),
83+
)),
84+
);
85+
});
86+
87+
test('listColumnNames throws StateError', () {
88+
expect(
89+
() => conn.listColumnNames(database: 'db', table: 'tbl'),
90+
throwsA(isA<StateError>().having(
91+
(e) => e.message,
92+
'message',
93+
contains('Not connected to MySQL'),
94+
)),
95+
);
96+
});
97+
98+
test('listTables throws StateError', () {
99+
expect(
100+
() => conn.listTables(schema: 'db'),
101+
throwsA(isA<StateError>().having(
102+
(e) => e.message,
103+
'message',
104+
contains('Not connected to MySQL'),
105+
)),
106+
);
107+
});
108+
109+
test('serverVersion throws StateError', () {
110+
expect(
111+
() => conn.serverVersion(),
112+
throwsA(isA<StateError>().having(
113+
(e) => e.message,
114+
'message',
115+
contains('Not connected to MySQL'),
116+
)),
117+
);
118+
});
119+
120+
test('serverStats throws StateError', () {
121+
expect(
122+
() => conn.serverStats(),
123+
throwsA(isA<StateError>().having(
124+
(e) => e.message,
125+
'message',
126+
contains('Not connected to MySQL'),
127+
)),
128+
);
129+
});
130+
});
42131
}

0 commit comments

Comments
 (0)