Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- **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.
- **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.

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

Expand Down
20 changes: 10 additions & 10 deletions lib/core/database/mysql_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,7 @@ class MysqlConnection {
bool get _usesConnectionString =>
connectionString != null && connectionString!.trim().isNotEmpty;

static String _escapeSqlString(String s) {
return s.replaceAll(r'\', r'\\').replaceAll("'", "''");
}


/// MySQL identifier quoting (backticks).
static String quoteIdentifier(String id) {
Expand Down Expand Up @@ -284,11 +282,11 @@ class MysqlConnection {
if (!isConnected || _conn == null) {
throw StateError('Not connected to MySQL');
}
final s = _escapeSqlString(schema);
final rs = await execute(
'SELECT TABLE_NAME FROM information_schema.TABLES '
"WHERE TABLE_SCHEMA = '$s' AND TABLE_TYPE = 'VIEW' "
"WHERE TABLE_SCHEMA = :schema AND TABLE_TYPE = 'VIEW' "
'ORDER BY TABLE_NAME',
{'schema': schema},
);
return rs.rows.map((r) => r.colAt(0)!).toList();
}
Expand All @@ -301,12 +299,14 @@ class MysqlConnection {
if (!isConnected || _conn == null) {
throw StateError('Not connected to MySQL');
}
final d = _escapeSqlString(database);
final t = _escapeSqlString(table);
final rs = await execute(
'SELECT COLUMN_NAME FROM information_schema.COLUMNS '
"WHERE TABLE_SCHEMA = '$d' AND TABLE_NAME = '$t' "
"WHERE TABLE_SCHEMA = :database AND TABLE_NAME = :table "
'ORDER BY ORDINAL_POSITION',
{
'database': database,
'table': table,
},
);
return rs.rows.map((r) => r.colAt(0)!).toList();
}
Expand All @@ -316,11 +316,11 @@ class MysqlConnection {
if (!isConnected || _conn == null) {
throw StateError('Not connected to MySQL');
}
final s = _escapeSqlString(schema);
final rs = await execute(
'SELECT TABLE_NAME FROM information_schema.TABLES '
"WHERE TABLE_SCHEMA = '$s' AND TABLE_TYPE = 'BASE TABLE' "
"WHERE TABLE_SCHEMA = :schema AND TABLE_TYPE = 'BASE TABLE' "
'ORDER BY TABLE_NAME',
{'schema': schema},
);
return rs.rows.map((r) => r.colAt(0)!).toList();
}
Expand Down
89 changes: 89 additions & 0 deletions test/core/database/mysql_connection_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,93 @@ void main() {
);
});
});

group('MysqlConnection when not connected', () {
late MysqlConnection conn;

setUp(() {
conn = MysqlConnection(
id: 1,
name: 'test',
host: 'localhost',
);
});

test('execute throws StateError', () {
expect(
() => conn.execute('SELECT 1'),
throwsA(isA<StateError>().having(
(e) => e.message,
'message',
contains('Not connected to MySQL'),
)),
);
});

test('listDatabases throws StateError', () {
expect(
() => conn.listDatabases(),
throwsA(isA<StateError>().having(
(e) => e.message,
'message',
contains('Not connected to MySQL'),
)),
);
});

test('listViews throws StateError', () {
expect(
() => conn.listViews(schema: 'db'),
throwsA(isA<StateError>().having(
(e) => e.message,
'message',
contains('Not connected to MySQL'),
)),
);
});

test('listColumnNames throws StateError', () {
expect(
() => conn.listColumnNames(database: 'db', table: 'tbl'),
throwsA(isA<StateError>().having(
(e) => e.message,
'message',
contains('Not connected to MySQL'),
)),
);
});

test('listTables throws StateError', () {
expect(
() => conn.listTables(schema: 'db'),
throwsA(isA<StateError>().having(
(e) => e.message,
'message',
contains('Not connected to MySQL'),
)),
);
});

test('serverVersion throws StateError', () {
expect(
() => conn.serverVersion(),
throwsA(isA<StateError>().having(
(e) => e.message,
'message',
contains('Not connected to MySQL'),
)),
);
});

test('serverStats throws StateError', () {
expect(
() => conn.serverStats(),
throwsA(isA<StateError>().having(
(e) => e.message,
'message',
contains('Not connected to MySQL'),
)),
);
});
});
}
Loading