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