1#!@PERL_PATH@ 2# -*- cperl -*- 3# 4# Copyright (c) 2007, 2017, Oracle and/or its affiliates. 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; version 2 of the License. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA 18 19use Fcntl; 20use File::Spec; 21use if $^O eq 'MSWin32', 'Term::ReadKey' => qw/ReadMode/; 22use strict; 23 24my $config = ".my.cnf.$$"; 25my $command = ".mysql.$$"; 26my $hadpass = 0; 27my $mysql; # How to call the mysql client 28my $rootpass = ""; 29 30 31$SIG{QUIT} = $SIG{INT} = $SIG{TERM} = $SIG{ABRT} = $SIG{HUP} = sub { 32 print "\nAborting!\n\n"; 33 echo_on(); 34 cleanup(); 35 exit 1; 36}; 37 38 39END { 40 # Remove temporary files, even if exiting via die(), etc. 41 cleanup(); 42} 43 44 45sub read_without_echo { 46 my ($prompt) = @_; 47 print $prompt; 48 echo_off(); 49 my $answer = <STDIN>; 50 echo_on(); 51 print "\n"; 52 chomp($answer); 53 return $answer; 54} 55 56sub echo_on { 57 if ($^O eq 'MSWin32') { 58 ReadMode('normal'); 59 } else { 60 system("stty echo"); 61 } 62} 63 64sub echo_off { 65 if ($^O eq 'MSWin32') { 66 ReadMode('noecho'); 67 } else { 68 system("stty -echo"); 69 } 70} 71 72sub write_file { 73 my $file = shift; 74 -f $file or die "ERROR: file is missing \"$file\": $!"; 75 open(FILE, ">$file") or die "ERROR: can't write to file \"$file\": $!"; 76 foreach my $line ( @_ ) { 77 print FILE $line, "\n"; # Add EOL char 78 } 79 close FILE; 80} 81 82sub prepare { 83 # Locate the mysql client; look in current directory first, then 84 # in path 85 our $SAVEERR; # Suppress Perl warning message 86 open SAVEERR, ">& STDERR"; 87 close STDERR; 88 for my $m (File::Spec->catfile('bin', 'mysql'), 'mysql') { 89 # mysql --version should always work 90 qx($m --no-defaults --version); 91 next unless $? == 0; 92 93 $mysql = $m; 94 last; 95 } 96 open STDERR, ">& SAVEERR"; 97 98 die "Can't find a 'mysql' client in PATH or ./bin\n" 99 unless $mysql; 100 101 # Create safe files to avoid leaking info to other users 102 foreach my $file ( $config, $command ) { 103 next if -f $file; # Already exists 104 local *FILE; 105 sysopen(FILE, $file, O_CREAT, 0600) 106 or die "ERROR: can't create $file: $!"; 107 close FILE; 108 } 109} 110 111# Simple escape mechanism (\-escape any ' and \), suitable for two contexts: 112# - single-quoted SQL strings 113# - single-quoted option values on the right hand side of = in my.cnf 114# 115# These two contexts don't handle escapes identically. SQL strings allow 116# quoting any character (\C => C, for any C), but my.cnf parsing allows 117# quoting only \, ' or ". For example, password='a\b' quotes a 3-character 118# string in my.cnf, but a 2-character string in SQL. 119# 120# This simple escape works correctly in both places. 121sub basic_single_escape { 122 my ($str) = @_; 123 # Inside a character class, \ is not special; this escapes both \ and ' 124 $str =~ s/([\'])/\\$1/g; 125 return $str; 126} 127 128sub do_query { 129 my $query = shift; 130 write_file($command, $query); 131 my $rv = system("$mysql --defaults-file=$config < $command"); 132 # system() returns -1 if exec fails (e.g., command not found, etc.); die 133 # in this case because nothing is going to work 134 die "Failed to execute mysql client '$mysql'\n" if $rv == -1; 135 # Return true if query executed OK, or false if there was some problem 136 # (for example, SQL error or wrong password) 137 return ($rv == 0 ? 1 : undef); 138} 139 140sub make_config { 141 my $password = shift; 142 143 my $esc_pass = basic_single_escape($rootpass); 144 write_file($config, 145 "# mysql_secure_installation config file", 146 "[mysql]", 147 "user=root", 148 "password='$esc_pass'"); 149} 150 151sub get_root_password { 152 my $attempts = 3; 153 for (;;) { 154 my $password = read_without_echo("Enter current password for root (enter for none): "); 155 if ( $password ) { 156 $hadpass = 1; 157 } else { 158 $hadpass = 0; 159 } 160 $rootpass = $password; 161 make_config($rootpass); 162 last if do_query(""); 163 164 die "Unable to connect to the server as root user, giving up.\n" 165 if --$attempts == 0; 166 } 167 print "OK, successfully used password, moving on...\n\n"; 168} 169 170sub set_root_password { 171 my $password1; 172 for (;;) { 173 $password1 = read_without_echo("New password: "); 174 175 if ( !$password1 ) { 176 print "Sorry, you can't use an empty password here.\n\n"; 177 next; 178 } 179 180 my $password2 = read_without_echo("Re-enter new password: "); 181 182 if ( $password1 ne $password2 ) { 183 print "Sorry, passwords do not match.\n\n"; 184 next; 185 } 186 187 last; 188 } 189 190 my $esc_pass = basic_single_escape($password1); 191 do_query("UPDATE mysql.user SET Password=PASSWORD('$esc_pass') WHERE User='root';") 192 or die "Password update failed!\n"; 193 194 print "Password updated successfully!\n"; 195 print "Reloading privilege tables..\n"; 196 reload_privilege_tables() 197 or die "Can not continue.\n"; 198 199 print "\n"; 200 $rootpass = $password1; 201 make_config($rootpass); 202} 203 204sub remove_anonymous_users { 205 do_query("DELETE FROM mysql.user WHERE User='';") 206 or die print " ... Failed!\n"; 207 print " ... Success!\n"; 208} 209 210sub remove_remote_root { 211 if (do_query("DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');")) { 212 print " ... Success!\n"; 213 } else { 214 print " ... Failed!\n"; 215 } 216} 217 218sub remove_test_database { 219 print " - Dropping test database...\n"; 220 if (do_query("DROP DATABASE IF EXISTS test;")) { 221 print " ... Success!\n"; 222 } else { 223 print " ... Failed! Not critical, keep moving...\n"; 224 } 225 226 print " - Removing privileges on test database...\n"; 227 if (do_query("DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%'")) { 228 print " ... Success!\n"; 229 } else { 230 print " ... Failed! Not critical, keep moving...\n"; 231 } 232} 233 234sub reload_privilege_tables { 235 if (do_query("FLUSH PRIVILEGES;")) { 236 print " ... Success!\n"; 237 return 1; 238 } else { 239 print " ... Failed!\n"; 240 return undef; 241 } 242} 243 244sub cleanup { 245 print "Cleaning up...\n"; 246 247 foreach my $file ($config, $command) { 248 unlink $file or warn "Warning: Could not unlink $file: $!\n"; 249 } 250} 251 252 253# The actual script starts here 254 255prepare(); 256 257print <<HERE; 258 259 260 261NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MySQL 262 SERVERS IN PRODUCTION USE! PLEASE READ EACH STEP CAREFULLY! 263 264In order to log into MySQL to secure it, we'll need the current 265password for the root user. If you've just installed MySQL, and 266you haven't set the root password yet, the password will be blank, 267so you should just press enter here. 268 269HERE 270 271get_root_password(); 272 273 274# 275# Set the root password 276# 277 278print "Setting the root password ensures that nobody can log into the MySQL\n"; 279print "root user without the proper authorisation.\n\n"; 280 281if ( $hadpass == 0 ) { 282 print "Set root password? [Y/n] "; 283} else { 284 print "You already have a root password set, so you can safely answer 'n'.\n\n"; 285 print "Change the root password? [Y/n] "; 286} 287 288my $reply = <STDIN>; 289if ( $reply =~ /n/i ) { 290 print " ... skipping.\n"; 291} else { 292 set_root_password(); 293} 294print "\n"; 295 296 297# 298# Remove anonymous users 299# 300 301print <<HERE; 302By default, a MySQL installation has an anonymous user, allowing anyone 303to log into MySQL without having to have a user account created for 304them. This is intended only for testing, and to make the installation 305go a bit smoother. You should remove them before moving into a 306production environment. 307 308HERE 309 310print "Remove anonymous users? [Y/n] "; 311$reply = <STDIN>; 312if ( $reply =~ /n/i ) { 313 print " ... skipping.\n"; 314} else { 315 remove_anonymous_users(); 316} 317print "\n"; 318 319 320# 321# Disallow remote root login 322# 323 324print <<HERE; 325Normally, root should only be allowed to connect from 'localhost'. This 326ensures that someone cannot guess at the root password from the network. 327 328HERE 329 330print "Disallow root login remotely? [Y/n] "; 331$reply = <STDIN>; 332if ( $reply =~ /n/i ) { 333 print " ... skipping.\n"; 334} else { 335 remove_remote_root(); 336} 337print "\n"; 338 339 340# 341# Remove test database 342# 343 344print <<HERE; 345By default, MySQL comes with a database named 'test' that anyone can 346access. This is also intended only for testing, and should be removed 347before moving into a production environment. 348 349HERE 350 351print "Remove test database and access to it? [Y/n] "; 352$reply = <STDIN>; 353if ( $reply =~ /n/i ) { 354 print " ... skipping.\n"; 355} else { 356 remove_test_database(); 357} 358print "\n"; 359 360 361# 362# Reload privilege tables 363# 364 365print <<HERE; 366Reloading the privilege tables will ensure that all changes made so far 367will take effect immediately. 368 369HERE 370 371print "Reload privilege tables now? [Y/n] "; 372$reply = <STDIN>; 373if ( $reply =~ /n/i ) { 374 print " ... skipping.\n"; 375} else { 376 reload_privilege_tables(); 377} 378print "\n"; 379 380print <<HERE; 381 382 383 384All done! If you've completed all of the above steps, your MySQL 385installation should now be secure. 386 387Thanks for using MySQL! 388 389 390HERE 391