PDO MySQL: Use PDO :: ATTR_EMULATE_PREPARES ou não?

117

Isto é o que li até agora sobre PDO::ATTR_EMULATE_PREPARES:

  1. A emulação de preparação do PDO é melhor para desempenho, pois a preparação nativa do MySQL ignora o cache de consulta .
  2. A preparação nativa do MySQL é melhor para segurança (evitando injeção de SQL) .
  3. A preparação nativa do MySQL é melhor para relatórios de erros .

Não sei mais o quão verdadeira qualquer uma dessas afirmações é. Minha maior preocupação ao escolher uma interface MySQL é prevenir a injeção de SQL. A segunda preocupação é o desempenho.

Meu aplicativo atualmente usa MySQLi procedural (sem instruções preparadas) e utiliza bastante o cache de consulta. Raramente reutilizará instruções preparadas em uma única solicitação. Comecei a mudança para PDO para os parâmetros nomeados e segurança das instruções preparadas.

Estou usando MySQL 5.1.61ePHP 5.3.2

Devo deixar PDO::ATTR_EMULATE_PREPARESativado ou não? Existe uma maneira de ter o desempenho do cache de consulta e a segurança das instruções preparadas?

Andrew Ensley
fonte
3
Honestamente? Continue usando o MySQLi. Se já estiver funcionando usando instruções preparadas sob isso, o PDO é basicamente uma camada de abstração inútil. EDIT : PDO é realmente útil para aplicações de campo verde onde você não tem certeza de qual banco de dados está indo para o back-end.
jmkeyes
1
Desculpe, minha pergunta não estava clara antes. Eu editei. O aplicativo não usa instruções preparadas no MySQLi no momento; apenas mysqli_run_query (). Pelo que li, as instruções preparadas pelo MySQLi também ignoram o cache de consulta.
Andrew Ensley

Respostas:

108

Para responder às suas preocupações:

  1. MySQL> = 5.1.17 (ou> = 5.1.21 para as instruções PREPAREe EXECUTE) pode usar instruções preparadas no cache de consulta . Portanto, sua versão do MySQL + PHP pode usar instruções preparadas com o cache de consulta. No entanto, observe cuidadosamente as advertências para os resultados das consultas em cache na documentação do MySQL. Existem muitos tipos de consultas que não podem ser armazenadas em cache ou que são inúteis, embora estejam armazenadas em cache. Na minha experiência, o cache de consulta geralmente não é uma grande vitória. Consultas e esquemas precisam de construção especial para aproveitar ao máximo o cache. Freqüentemente, o armazenamento em cache no nível do aplicativo acaba sendo necessário de qualquer maneira a longo prazo.

  2. A preparação nativa não faz diferença para a segurança. As instruções pseudo-preparadas ainda escaparão dos valores dos parâmetros de consulta, isso apenas será feito na biblioteca PDO com strings em vez de no servidor MySQL usando o protocolo binário. Em outras palavras, o mesmo código PDO será igualmente vulnerável (ou não vulnerável) a ataques de injeção, independentemente de sua EMULATE_PREPARESconfiguração. A única diferença é onde ocorre a substituição do parâmetro - com EMULATE_PREPARES, ela ocorre na biblioteca PDO; sem EMULATE_PREPARES, ele ocorre no servidor MySQL.

  3. Sem EMULATE_PREPARESvocê pode obter erros de sintaxe no tempo de preparação em vez de no tempo de execução; com EMULATE_PREPARESvocê só obterá erros de sintaxe em tempo de execução porque PDO não tem uma consulta para dar ao MySQL até o tempo de execução. Observe que isso afeta o código que você escreverá ! Especialmente se você estiver usando PDO::ERRMODE_EXCEPTION!

Uma consideração adicional:

  • Há um custo fixo para a prepare()(usando instruções preparadas nativas), portanto, a prepare();execute()com instruções preparadas nativas pode ser um pouco mais lento do que emitir uma consulta de texto simples usando instruções preparadas emuladas. Em muitos sistemas de banco de dados, o plano de consulta para a também prepare()é armazenado em cache e pode ser compartilhado com várias conexões, mas não acho que o MySQL faça isso. Portanto, se você não reutilizar seu objeto de instrução preparado para várias consultas, sua execução geral pode ser mais lenta.

Como recomendação final , acho que com versões anteriores do MySQL + PHP, você deve emular instruções preparadas, mas com suas versões mais recentes você deve desligar a emulação.

Depois de escrever alguns aplicativos que usam PDO, fiz uma função de conexão PDO que tem o que considero ser as melhores configurações. Você provavelmente deve usar algo assim ou ajustar suas configurações preferidas:

/**
 * Return PDO handle for a MySQL connection using supplied settings
 *
 * Tries to do the right thing with different php and mysql versions.
 *
 * @param array $settings with keys: host, port, unix_socket, dbname, charset, user, pass. Some may be omitted or NULL.
 * @return PDO
 * @author Francis Avila
 */
function connect_PDO($settings)
{
    $emulate_prepares_below_version = '5.1.17';

    $dsndefaults = array_fill_keys(array('host', 'port', 'unix_socket', 'dbname', 'charset'), null);
    $dsnarr = array_intersect_key($settings, $dsndefaults);
    $dsnarr += $dsndefaults;

    // connection options I like
    $options = array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    );

    // connection charset handling for old php versions
    if ($dsnarr['charset'] and version_compare(PHP_VERSION, '5.3.6', '<')) {
        $options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES '.$dsnarr['charset'];
    }
    $dsnpairs = array();
    foreach ($dsnarr as $k => $v) {
        if ($v===null) continue;
        $dsnpairs[] = "{$k}={$v}";
    }

    $dsn = 'mysql:'.implode(';', $dsnpairs);
    $dbh = new PDO($dsn, $settings['user'], $settings['pass'], $options);

    // Set prepared statement emulation depending on server version
    $serverversion = $dbh->getAttribute(PDO::ATTR_SERVER_VERSION);
    $emulate_prepares = (version_compare($serverversion, $emulate_prepares_below_version, '<'));
    $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, $emulate_prepares);

    return $dbh;
}
Francis Avila
fonte
26
Referente ao nº 2: certamente os valores que o MySQL recebe como parâmetros (para instruções preparadas nativas) não são analisados ​​para SQL de forma alguma ? Portanto, o risco de injeção deve ser menor do que usar a emulação de preparação do PDO, onde qualquer falha no escape (por exemplo, os problemas históricos mysql_real_escape_stringcom caracteres multibyte) ainda deixaria alguém aberto a ataques de injeção?
Eggyal
2
@eggyal, você está fazendo suposições sobre como as declarações preparadas são implementadas. PDO pode ter um bug em seu escape de preparação emulado, mas o MySQL pode ter bugs também. AFAIK, nenhum problema foi descoberto com preparações emuladas que poderiam fazer com que literais de parâmetro passassem sem escape.
Francis Avila
2
Resposta incrível, mas tenho uma pergunta: se você desligar a EMULAÇÃO, a execução não será mais lenta? O PHP teria que enviar a instrução preparada ao MySQL para validação e só então enviar os parâmetros. Portanto, se você usar a instrução preparada 5 vezes, o PHP se comunicará com o MySQL 6 vezes (em vez de 5). Isso não vai torná-lo mais lento? Além disso, acho que há uma chance maior de que o PDO possa ter bugs no processo de validação, em vez do MySQL ...
Radu Murzea
6
Observe os pontos feitos nesta resposta repreparada emulação de instrução usando o mysql_real_escape_stringunder the capô e as vulnerabilidades consequentes que podem surgir (em casos muito específicos).
Eggyal
6
+1 Boa resposta! Mas, para registro, se você usar a preparação nativa, os parâmetros nunca serão escapados ou combinados na consulta SQL, mesmo no lado do servidor MySQL. No momento em que você executa e fornece parâmetros, a consulta foi analisada e transformada em estruturas de dados internas no MySQL. Leia este blog de um engenheiro otimizador do MySQL que explica esse processo: guilhembichot.blogspot.com/2014/05/… Não estou dizendo que isso significa que a preparação nativa é melhor, na medida em que confiamos no código PDO para fazer o escape corretamente (o que eu Faz).
Bill Karwin
9

Cuidado ao desabilitar PDO::ATTR_EMULATE_PREPARES(ativar preparações nativas) quando seu PHP pdo_mysqlnão for compilado mysqlnd.

Como o antigo libmysqlnão é totalmente compatível com algumas funções, pode levar a bugs estranhos, por exemplo:

  1. Perda de bits mais significativos para inteiros de 64 bits ao vincular como PDO::PARAM_INT(0x12345678AB será cortado para 0x345678AB na máquina de 64 bits)
  2. Incapacidade de fazer consultas simples como LOCK TABLES(lança SQLSTATE[HY000]: General error: 2030 This command is not supported in the prepared statement protocol yetexceção)
  3. Precisa buscar todas as linhas do resultado ou fechar o cursor antes da próxima consulta (com mysqlndou emulado prepara isso automaticamente faz este trabalho para você e não sai de sincronia com o servidor mysql)

Esses bugs descobri em meu projeto simples quando migrei para outro servidor que usava libmysqlpara o pdo_mysqlmódulo. Talvez haja muito mais bugs, não sei. Também testei no debian jessie de 64 bits fresco, todos os bugs listados ocorrem quando eu apt-get install php5-mysql, e desaparecem quando eu apt-get install php5-mysqlnd.

Quando PDO::ATTR_EMULATE_PREPARESé definido como verdadeiro (como padrão) - esses bugs não acontecem de qualquer maneira, porque o PDO não usa instruções preparadas neste modo. Portanto, se você usar o pdo_mysqlbaseado em libmysql(substring "mysqlnd" não aparece no campo "Versão da API do cliente" da pdo_mysqlseção no phpinfo) - você não deve PDO::ATTR_EMULATE_PREPARESdesligar.

Sage Pointer
fonte
3
essa preocupação ainda é válida em 2019 ?!
oldboy
8

Eu desligaria as preparações de emulação enquanto você está executando o 5.1, o que significa que o PDO aproveitará a funcionalidade de instrução preparada nativa.

PDO_MYSQL aproveitará a vantagem do suporte de instrução preparada nativa presente no MySQL 4.1 e superior. Se você estiver usando uma versão mais antiga das bibliotecas de cliente mysql, o PDO irá emulá-las para você.

http://php.net/manual/en/ref.pdo-mysql.php

Eu troquei o MySQLi pelo PDO para as declarações nomeadas preparadas e a melhor API.

No entanto, para ser equilibrado, o PDO tem um desempenho insignificante mais lento do que o MySQLi, mas é algo para se ter em mente. Eu sabia disso quando fiz a escolha e decidi que uma API melhor e usar o padrão da indústria era mais importante do que usar uma biblioteca desprezivelmente mais rápida que conecta você a um mecanismo específico. FWIW Acho que a equipe do PHP também está olhando favoravelmente para o PDO em vez do MySQLi para o futuro.

Will Morgan
fonte
Obrigado por essa informação. Como não poder usar o cache de consulta afetou seu desempenho ou você já o usava antes?
Andrew Ensley
Não posso dizer que estou usando caches em vários níveis de qualquer maneira. No entanto, você sempre pode usar explicitamente SELECT SQL_CACHE <resto da instrução>.
Will Morgan
Nem sabia que havia uma opção SELECT SQL_CACHE. No entanto, parece que isso ainda não funcionaria. Dos documentos: "O resultado da consulta é armazenado em cache se puder ser armazenado em cache ..." dev.mysql.com/doc/refman/5.1/en/query-cache-in-select.html
Andrew Ensley
Sim. Isso depende da natureza da consulta, e não das especificações da plataforma.
Will Morgan
Eu li isso para significar "O resultado da consulta é armazenado em cache, a menos que algo o impeça de ser armazenado em cache ", o que - pelo que eu tinha lido até então - incluía instruções preparadas. No entanto, graças à resposta de Francis Avila, sei que isso não é mais verdade para a minha versão do MySQL.
Andrew Ensley
6

Eu recomendo habilitar PREPAREchamadas de banco de dados reais , pois a emulação não pega tudo .., por exemplo, ela irá preparar INSERT;!

var_dump($dbh->prepare('INSERT;'));
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
var_dump($dbh->prepare('INSERT;'));

A saída

object(PDOStatement)#2 (1) {
  ["queryString"]=>
  string(7) "INSERT;"
}
bool(false)

Terei prazer em obter um impacto sobre o desempenho do código que realmente funciona.

FWIW

Versão do PHP: PHP 5.4.9-4ubuntu2.4 (cli)

Versão do MySQL: 5.5.34-0ubuntu0

troca rápida
fonte
É um ponto interessante. Eu acho que a emulação adia a análise do lado do servidor para a fase de execução. Embora não seja um grande problema (SQL errado eventualmente irá falhar), é mais limpo para deixar preparefazer o trabalho que deveria. (Além disso, sempre assumi que o analisador de parâmetros do lado do cliente necessariamente terá seus próprios bugs.)
Álvaro González
1
IDK se você estiver interessado, mas aqui está um pequeno artigo sobre algum outro comportamento espúrio que observei com o PDO que me levou até a toca do coelho, para começar. Parece que falta o tratamento de várias consultas.
quickshift em
Acabei de olhar algumas bibliotecas de migração no GitHub ... O que você sabe, esta faz exatamente a mesma coisa que minha postagem no blog.
quickshift em
5

Por que mudar a emulação para 'false'?

A principal razão para isso é que ter o mecanismo de banco de dados fazendo a preparação em vez de PDO é que a consulta e os dados reais são enviados separadamente, o que aumenta a segurança. Isso significa que quando os parâmetros são passados ​​para a consulta, as tentativas de injetar SQL neles são bloqueadas, uma vez que as instruções preparadas pelo MySQL são limitadas a uma única consulta. Isso significa que uma instrução preparada verdadeira falhará quando for passada uma segunda consulta em um parâmetro.

O principal argumento contra o uso do mecanismo de banco de dados para a preparação versus PDO são as duas viagens ao servidor - uma para a preparação e outra para os parâmetros serem passados ​​- mas acho que a segurança adicional vale a pena. Além disso, pelo menos no caso do MySQL, o cache de consulta não é um problema desde a versão 5.1.

https://tech.michaelseiler.net/2016/07/04/dont-emulate-prepared-statements-pdo-mysql/

Harry Bosh
fonte
1
O cache de consulta não existe mais : O cache de consulta está obsoleto no MySQL 5.7.20 e foi removido no MySQL 8.0.
Álvaro González
5

Estou surpreso que ninguém tenha mencionado um dos maiores motivos para desativar a emulação. Com a emulação ativada, o PDO retorna todos os inteiros e flutuantes como strings . Quando você desativa a emulação, inteiros e flutuantes no MySQL tornam-se inteiros e flutuantes no PHP.

Para obter mais informações, consulte a resposta aceita para esta pergunta: PHP + PDO + MySQL: como faço para retornar colunas inteiras e numéricas do MySQL como inteiros e numéricos em PHP? .

dallin
fonte
0

Para o registro

PDO :: ATTR_EMULATE_PREPARES = true

Isso pode gerar um efeito colateral desagradável. Ele pode retornar valores inteiros como string.

PHP 7.4, pdo com mysqlnd.

Executando uma consulta com PDO :: ATTR_EMULATE_PREPARES = true

Coluna: id
Tipo: inteiro
Valor: 1

Executando uma consulta com PDO :: ATTR_EMULATE_PREPARES = false

Coluna: id
Tipo: string
Valor: "1"

Em qualquer caso, os valores decimais são sempre retornados como uma string, independentemente da configuração :-(

Magalhães
fonte
os valores decimais sempre são retornados, uma string é a única maneira correta
Seu senso comum
Sim, do ponto de vista do MySQL, mas está errado no lado do PHP. Java e C # consideram decimal como um valor numérico.
Magallanes
Não, não é. Tudo correto para toda a ciência da computação. Se você acha que está errado, então você precisa de outro tipo, de precisão arbitrária
Your Common Sense