快速插入的准备好的语句

时间:2018-08-26 10:40:45

标签: mysql perl

简而言之

Perl中是否可以使用准备好的语句(以防止SQL注入)在不到2分钟的时间内将100万条记录插入MySQL表中?


详细信息

有一个在线资源(Wikimedia),我想从中下载包含将近一百万个文件( dewiktionary-latest-all-titles-in-ns0.gz )的文件文章标题(每篇文章都是对Wiktionary中的德语单词的描述)。我想每周检查一次此列表,然后对新标题或删除的标题做出反应。为此,我想每周自动下载一次此列表,然后将其插入数据库中。

尽管我信任Wikimedia,但您永远不应信任任何来自互联网的内容。因此,为了防止SQL注入和其他安全问题,我总是在Perl中使用准备好的语句,请确保SQL解释器没有机会将内容解释为代码。

通常我会这样:

程序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);

请注意此行(第27行):

my $SQL = 'INSERT INTO `mytable`(`word`) VALUES(?)';

SQL命令行中有一个问号作为占位符。 在下一行中,将准备此SQL命令行(即,创建一条准备好的语句),并在循环中执行该语句,这意味着每次将新值($ word)插入表时,都无需执行此值的任何机会,因为SQL解释器看不到该值。因此,无论攻击者将什么内容写入我下载的文件中,都永远不会导致代码注入。

但是:
这很慢。下载在几秒钟内完成,但是插入循环运行了四个多小时。


有一个更快的解决方案,它是这样的:

程序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);

(这是简化的,因为该SQL语句将变得太长而无法被MySQL接受。您需要将其分成大约5000个值的部分并执行它们。但这对我在这里谈论的问题。)

运行非常快。在不到2分钟的时间内插入了所有值(新表中几乎有100万行),这快100倍以上。

如您所见,我创建了一个大声明,但没有占位符。我直接将值写入SQL命令。我只需要转义将被解释为转义字符的反斜杠和将被解释为字符串结尾的单引号。

但是其余的值仍然不受保护,并且对于SQL解释器可见。潜在的攻击者可能会找到一种将SQL代码插入将要执行的值的方法。这可能会损坏我的数据库,甚至可能授予攻击者超级用户权限。 (代码注入导致的特权升级)


所以,这是我的问题:

有没有办法像程序1中那样使用准备好的语句,甚至对于像程序2中那样动态生成的语句呢?

或者还有快速且安全将大量数据插入MySQL表的可能性吗?

2 个答案:

答案 0 :(得分:3)

您用斜体字表示的小注释实际上很相关:

  

(这是简化的,因为SQL语句将变得太长而无法被MySQL接受。您需要将其拆分成大约5000个值的部分并执行它们。但这对于我遇到的问题并不重要在这里谈论。)

我认为您的“未准备的声明”(不是真实术语)方法更快,因为您一次一次批量加载5000条记录,而不是一次一次地批量加载,而不是因为它不是准备好的语句。

尝试使用5000 ?来构建准备好的语句,如下所示:

my $SQL = 'INSERT INTO `mytable`(`word`) VALUES ' . '(?),'x4999 . '(?)';

然后一次建立一个5000个单词的列表,并以此来执行准备好的语句。您必须使用第二个动态生成的准备好的语句来处理最后一组(大概)少于5000个单词,该语句由最后一批中适当数量的单词组成。

您还可以查看LOAD DATA INFILE进行批量加载。

答案 1 :(得分:1)

(此答案由问题的作者写成。)

e.dan通过his answer带给我正确的主意,所以谢谢e.dan!

这是使用准备好的语句的快速解决方案:

# The code above the SQL-Statement is exactly
# the same as in the 1st program in the question
#-------------------------------------------------
# SQL statement
my $SQL = 'INSERT INTO `mytable`(`word`) VALUES ';
# Counter
my $cnt   = 0;
# initiate comma with empty string
my $comma = '';
# An array to store the parameters (This array does the trick!)
my @param = ();
# loop through all words
foreach my $word (@list) {
    # (no escaping needed)
    # attach a question mark in brackets to the query string
    $SQL .= $comma."(?)";
    # and push the value into the parameter-array
    push(@param,$word);
    # next time it must be a comma
    $comma = ',';
    # increment the counter
    $cnt++;
    # limit reached?
    if ($cnt >= 5000) {
        # Yes, limit reached
        # prepare the string with 5000 question marks
        my $SH = $dbh->prepare($SQL);
        # hand over a list of 5000 values and execute the prepared statement
        # (for Perl a comma separated list and an array are equal
        # if used as parameter for a function call)
        $SH->execute(@param);
        # Reset the variables
        $SQL = 'INSERT INTO `mytable`(`word`) VALUES ';
        $cnt = 0;
        $comma = '';
        @param = ();
    }
}
# is there something left at the end?
if ($comma ne '') {
    # Yes, there is something left at the end
    # prepare the string with many (but less than 5000) question marks
    my $SH = $dbh->prepare($SQL);
    # hand over the list of values and execute the prepared statement
    $SH->execute(@param);
}
# disconnect from database
$dbh->disconnect;
# end of program
exit(0);

诀窍在于,当您在Perl中调用函数或方法时,可以将参数作为标量来传递,并用逗号分隔:

object->method($scalar1, $scalar2, $scalar3);

但是您也可以交出一个数组:

my $@array = ($scalar1, $scalar2, $scalar3);
object->method(@array);

因此,您可以使用数组来传递可变数量的参数,并且还可以轻松地传递5000个(甚至更多)参数。

顺便说一句:
根据我的问题,该版本甚至比版本2还要快。