Skip to content

Commit 2450ebb

Browse files
committed
fix(security): robust version-agnostic password column detection (Issue #22)
- Refactored password column detection into a standalone sub. - Implemented schema-based detection using information_schema.COLUMNS. - Added regression test tests/repro_issue_22.t. - Updated tests/test_issue_875.t mocks. - Updated Changelog.
1 parent 8e207f7 commit 2450ebb

9 files changed

Lines changed: 140 additions & 35 deletions

File tree

Changelog

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
2.8.36 2026-02-02
1+
2.8.36 2026-02-13
22

3+
- fix: robust, version-agnostic detection of password column in mysql.user via schema inspection (Issue #22).
4+
- fix: correct regression in tests/test_issue_875.t by updating database mocks.
5+
- test: add comprehensive test suite for password column detection (tests/repro_issue_22.t).
36
- fix: prevent creation of directory "0" when --dumpdir is not specified or set to 0 (Issue #20).
47
- test: add reproduction test for Performance Schema disabled scenario (repro_pfs_disabled.t).
58
- ci: fix Docker API mismatch in GitHub Actions by migrating to native services.

README.fr.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Les résultats des tests sont disponibles ici uniquement pour les versions LTS 
5656
* Cluster Percona XtraDB (prise en charge complète)
5757
* Réplication MySQL (prise en charge partielle, pas d'environnement de test)
5858

59-
Merci à [endoflife.date](endoflife.date)
59+
Merci à [endoflife.date](https://endoflife.date/)
6060

6161
* Reportez-vous aux [versions prises en charge de MariaDB](https://github.com/jmrenouard/MySQLTuner-perl/blob/master/mariadb_support.md).
6262
* Reportez-vous aux [versions prises en charge de MySQL](https://github.com/jmrenouard/MySQLTuner-perl/blob/master/mysql_support.md).

README.it.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ I risultati dei test sono disponibili qui solo per LTS:
5656
* Cluster Percona XtraDB (supporto completo)
5757
* Replica MySQL (supporto parziale, nessun ambiente di test)
5858

59-
Grazie a [endoflife.date](endoflife.date)
59+
Grazie a [endoflife.date](https://endoflife.date/)
6060

6161
* Fare riferimento a [Versioni supportate di MariaDB](https://github.com/jmrenouard/MySQLTuner-perl/blob/master/mariadb_support.md).
6262
* Fare riferimento a [Versioni supportate di MySQL](https://github.com/jmrenouard/MySQLTuner-perl/blob/master/mysql_support.md).

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Test result are available here for LTS only:
5656
* Percona XtraDB cluster (full support)
5757
* MySQL Replication (partial support, no test environment)
5858

59-
Thanks to [endoflife.date](endoflife.date)
59+
Thanks to [endoflife.date](https://endoflife.date/)
6060

6161
* Refer to [MariaDB Supported versions](https://github.com/jmrenouard/MySQLTuner-perl/blob/master/mariadb_support.md).
6262
* Refer to [MySQL Supported versions](https://github.com/jmrenouard/MySQLTuner-perl/blob/master/mysql_support.md).

README.ru.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ MySQLTuner нуждается в вас
5656
* Кластер Percona XtraDB (полная поддержка)
5757
* Репликация MySQL (частичная поддержка, нет тестовой среды)
5858

59-
Спасибо [endoflife.date](endoflife.date)
59+
Спасибо [endoflife.date](https://endoflife.date/)
6060

6161
* См. [Поддерживаемые версии MariaDB](https://github.com/jmrenouard/MySQLTuner-perl/blob/master/mariadb_support.md).
6262
* См. [Поддерживаемые версии MySQL](https://github.com/jmrenouard/MySQLTuner-perl/blob/master/mysql_support.md).
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Specification: Robust Password Column Detection in mysqltuner.pl
2+
3+
## Problem
4+
5+
`mysqltuner.pl` fails to detect the correct password column (`password` vs `authentication_string`) on MySQL 8.0+ because its detection logic is hardcoded for specific versions (5.7, MariaDB 10.2-10.5). This leads to failing SQL queries like:
6+
`SELECT ... FROM mysql.user WHERE (password = '' OR password IS NULL)`
7+
on versions where the `password` column no longer exists.
8+
9+
## Requirements
10+
11+
1. **Version Agnostic**: Detection must rely on actual schema inspection of `mysql.user` rather than hardcoded version numbers.
12+
2. **Compatibility**:
13+
- Support legacy `Password` (capital P) and modern `authentication_string` columns.
14+
- Handle instances where both might exist (e.g., during some MariaDB upgrades).
15+
- Stay agnostic to exact casing of the `Password` column.
16+
3. **Stability**:
17+
- Fail gracefully if no known authentication column is found.
18+
- Ensure the resulting SQL remains safe for all supported versions.
19+
20+
## Proposed Logic
21+
22+
1. Retrieve columns from `mysql.user` using `select_table_columns_db('mysql', 'user')`.
23+
2. Check for `authentication_string` and `password` (case-insensitive).
24+
3. Set `$PASS_COLUMN_NAME`:
25+
- If both exist: use `IF(plugin='mysql_native_password', authentication_string, password)`.
26+
- If only `authentication_string` exists: use `authentication_string`.
27+
- If only `password` exists: use the exact name found in the schema (e.g., `Password`).
28+
4. If none exist, log an info message and return early (skip password-related security checks).
29+
30+
## Success Criteria
31+
32+
- `mysqltuner.pl` executes security recommendations without SQL errors on MySQL 8.0.
33+
- `mysqltuner.pl` still works correctly on legacy MySQL 5.5/5.6.
34+
- `mysqltuner.pl` works correctly on MariaDB 10.11+.

mysqltuner.pl

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2167,6 +2167,34 @@ sub select_table_columns_db {
21672167
);
21682168
}
21692169

2170+
sub get_password_column_name {
2171+
my @mysql_user_columns = select_table_columns_db( 'mysql', 'user' );
2172+
my $pass_column = '';
2173+
my $auth_column = '';
2174+
2175+
if ( grep { /^authentication_string$/msx } @mysql_user_columns ) {
2176+
$auth_column = 'authentication_string';
2177+
}
2178+
2179+
# Case-insensitive match for Password/password
2180+
my @pass_matches = grep { lc($_) eq 'password' } @mysql_user_columns;
2181+
if (@pass_matches) {
2182+
$pass_column = $pass_matches[0];
2183+
}
2184+
2185+
if ( $auth_column && $pass_column ) {
2186+
return "IF(plugin='mysql_native_password', $auth_column, $pass_column)";
2187+
}
2188+
elsif ($auth_column) {
2189+
return $auth_column;
2190+
}
2191+
elsif ($pass_column) {
2192+
return $pass_column;
2193+
}
2194+
2195+
return '';
2196+
}
2197+
21702198
sub get_tuning_info {
21712199
my @infoconn = select_array "\\s";
21722200
my ( $tkey, $tval );
@@ -3359,36 +3387,11 @@ sub security_recommendations {
33593387

33603388
infoprint "$myvar{'version_comment'} - $myvar{'version'}";
33613389

3362-
my $PASS_COLUMN_NAME = 'password';
3390+
my $PASS_COLUMN_NAME = get_password_column_name();
33633391

3364-
# New table schema available since mysql-5.7 and mariadb-10.2
3365-
# But need to be checked
3366-
if (
3367-
( $myvar{'version'} =~ /5\.7/ )
3368-
or (
3369-
( $myvar{'version'} =~ /10\.[2-5]\..*/ )
3370-
and ( ( $myvar{'version'} =~ /MariaDB/i )
3371-
or ( $myvar{'version_comment'} =~ /MariaDB/i ) )
3372-
)
3373-
)
3374-
{
3375-
my $result_pass = execute_system_command(
3376-
"$mysqlcmd $mysqllogin -Bse \"SELECT 1 FROM information_schema.columns WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' AND COLUMN_NAME = 'password'\" 2>>$devnull"
3377-
);
3378-
my $result_auth = execute_system_command(
3379-
"$mysqlcmd $mysqllogin -Bse \"SELECT 1 FROM information_schema.columns WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' AND COLUMN_NAME = 'authentication_string'\" 2>>$devnull"
3380-
);
3381-
if ( $result_pass && $result_auth ) {
3382-
$PASS_COLUMN_NAME =
3383-
"IF(plugin='mysql_native_password', authentication_string, password)";
3384-
}
3385-
elsif ($result_auth) {
3386-
$PASS_COLUMN_NAME = 'authentication_string';
3387-
}
3388-
elsif ( !$result_pass ) {
3389-
infoprint "Skipped due to none of known auth columns exists";
3390-
return;
3391-
}
3392+
if ( $PASS_COLUMN_NAME eq '' ) {
3393+
infoprint "Skipped due to none of known auth columns exists";
3394+
return;
33923395
}
33933396
debugprint "Password column = $PASS_COLUMN_NAME";
33943397

tests/repro_issue_22.t

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use strict;
2+
use warnings;
3+
use Test::More;
4+
use File::Basename;
5+
use File::Spec;
6+
use Cwd 'abs_path';
7+
8+
# Load mysqltuner.pl as a library
9+
my $script_dir = dirname(abs_path(__FILE__));
10+
my $script = abs_path(File::Spec->catfile($script_dir, '..', 'mysqltuner.pl'));
11+
require $script;
12+
13+
# Mocking necessary database calls and variables
14+
no warnings 'redefine';
15+
16+
my $mocked_columns = [];
17+
*main::select_table_columns_db = sub { return @$mocked_columns };
18+
*main::execute_system_command = sub { return "" }; # Dummy
19+
*main::subheaderprint = sub { };
20+
*main::infoprint = sub { };
21+
*main::goodprint = sub { };
22+
*main::badprint = sub { };
23+
*main::debugprint = sub { };
24+
*main::select_one = sub { return 0 };
25+
*main::select_array = sub { return () };
26+
*main::mysql_version_le = sub { return 0 };
27+
*main::mysql_version_ge = sub { return 1 };
28+
29+
subtest 'Detection Logic - MySQL 5.5/5.6 (Password only)' => sub {
30+
$mocked_columns = ['Host', 'User', 'Password', 'Select_priv'];
31+
my $res = main::get_password_column_name();
32+
is($res, 'Password', 'Detected Password (capital P)');
33+
};
34+
35+
subtest 'Detection Logic - MySQL 5.5/5.6 (password lowercase)' => sub {
36+
$mocked_columns = ['Host', 'User', 'password', 'Select_priv'];
37+
my $res = main::get_password_column_name();
38+
is($res, 'password', 'Detected password (lowercase)');
39+
};
40+
41+
subtest 'Detection Logic - MySQL 8.0 (authentication_string only)' => sub {
42+
$mocked_columns = ['Host', 'User', 'authentication_string', 'Select_priv'];
43+
my $res = main::get_password_column_name();
44+
is($res, 'authentication_string', 'Detected authentication_string');
45+
};
46+
47+
subtest 'Detection Logic - MariaDB Mixed (both exist)' => sub {
48+
$mocked_columns = ['Host', 'User', 'Password', 'authentication_string', 'Select_priv'];
49+
my $res = main::get_password_column_name();
50+
is($res, "IF(plugin='mysql_native_password', authentication_string, Password)", 'Detected both and used IF(...)');
51+
};
52+
53+
subtest 'Detection Logic - None exist' => sub {
54+
$mocked_columns = ['Host', 'User', 'Select_priv'];
55+
my $res = main::get_password_column_name();
56+
is($res, '', 'Returned empty string when none exist');
57+
};
58+
59+
done_testing();

tests/test_issue_875.t

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ my $mock_login_success = 0;
4343

4444
# Mock select_one and select_array to avoid DB connection
4545
*main::select_one = sub { return 0; };
46-
*main::select_array = sub { return (); };
46+
*main::select_array = sub {
47+
my ($sql) = @_;
48+
if ($sql =~ /FROM information_schema\.COLUMNS WHERE TABLE_SCHEMA='mysql' AND TABLE_NAME='user'/) {
49+
return ('Host', 'User', 'authentication_string');
50+
}
51+
return ();
52+
};
4753
}
4854

4955
sub has_output {

0 commit comments

Comments
 (0)