In a nutshell
Is there a way in Perl to use prepared statements (to prevent SQL injection) to insert 1 million records within less than 2 minutes into a MySQL-table?
In detail
There is an online resource (Wikimedia) from which I want to download a file (dewiktionary-latest-all-titles-in-ns0.gz) that contains almost 1 million titles of articles (each article is a descriptions of a German word in Wiktionary). I want to check this list once a week and then react on new or deleted titles. To do this, I want to automatically download this list once a week and insert it into a database.
Although I trust Wikimedia, you never should trust anything coming from internet too much. So, to prevent SQL injection and other security issues, I always use prepared statements in Perl, do make sure, that the SQL interpreter has no chance to interpret content as code.
Normally I would do this this way:
program 1
#!/usr/bin/perl -w
use strict;
use warnings;
use LWP::UserAgent;
use DBI;
# DOWNLOAD FROM INTERNET =========================
# create User-Agent:
my $ua = LWP::UserAgent->new;
# read content from Internet
my $response = $ua->get('https://<rest_of_URL>');
# decode content
my $content = $response->decoded_content;
#turn into a list
my @list = split(/\n/,$content);
# STORE IN DATABASE ==============================
# connect with database (create DataBase-Handle):
my $dbh = DBI->connect(
'DBI:mysql:database=<name_of_DB>;host=localhost',
'<user>','<password>',
{mysql_enable_utf8mb4 => 1}
);
# SQL statement
my $SQL = 'INSERT INTO `mytable`(`word`) VALUES(?)';
# prepare statement (create Statement Handle)
my $SH = $dbh->prepare($SQL);
#execute in a loop
foreach my $word (@list) {
$SH->execute($word);
}
# disconnect from database
$dbh->disconnect;
# end of program
exit(0);
Pay attention to this line (line 27):
my $SQL = 'INSERT INTO `mytable`(`word`) VALUES(?)';
There is a question-mark as placeholder in the SQL command line. In the next line this SQL command line is prepared (i.e. an prepared statement is created), and in the loop this statement is executed, which means, that each time a new value ($word) will be inserted into the table, without having any chance to execute this value, because the SQL-interpreter doesn't see this value. So, whatever an attacker might write into the file I downloaded, it never will cause a code injection.
But:
This is very slow. The download is done within a few seconds, but the insert-loop is running for more than four hours.
There is a faster solution, it goes like this:
program 2
# The code above the SQL-Statement is exactly
# the same as in the 1st program
#-------------------------------------------------
# SQL statement
my $SQL = 'INSERT INTO `mytable`(`word`) VALUES '; # <== NO '?'!
# attach values in a loop
# initiate comma with empty string
my $comma = '';
foreach my $word (@list) {
# escape escapecharacter
$word =~ s/\\/\\\\/g;
# escape quotes
$word =~ s/'/\\'/g;
# put the value in quotes and then in brackets, add the comma
# and then append it to the SQL command string
$SQL .= $comma."('".$word."')";
# comma must be a comma
$comma = ',';
}
# Now prepare this mega-statement
my $SH = $dbh->prepare($SQL);
# and execute it without any parameter
$SH->execute();
# disconnect from database
$dbh->disconnect;
# end of program
exit(0);
(This is simplified, since the SQL statement would become too long to be accepted by MySQL. You need to split it up in sections of about 5000 values and execute them. But this is not important for the problem I'm talking about here.)
This runs very fast. All values (almost 1 million rows in the new table) are inserted within less than 2 minutes, this is more than 100 times faster.
As you can see, I create one big statement, but without placeholders. I write the values directly into the SQL command. I just needed to escape backslashes that would be interpreted as escape characters and single quotation marks that would be interpreted as the end of a string.
But the rest of the values remains unprotected and is visible to the SQL interpreter. A potential attacker might find a way to insert SQL code into the values that will be executed. This can damage my database, or it even might grant the attacker superuser rights. (privilege escalation induced by code injection)
So, here is my question:
Is there a way to use prepared statements like in program 1 even for statements that are dynamically generated like in program 2?
Or is there another possibility to fast and secure insert big amounts of data into a MySQL table?