Dia 13 — SQL injection: o erro que não se comete

Informatica · Conteudo · publicado em 30/09/2026
Dia 13 de 16

SQL injection: o erro que não se comete

Aula 1

O que é SQL injection e como acontece

SQL injection: quando o valor vira sintaxe

O SQL não é uma string. É uma linguagem, com gramática, e o servidor lê a

frase inteira que você mandou. Quando o valor do cliente entra colado no texto,

o cliente passou a escrever a frase junto — e a frase dele é válida.

Isso se chama injeção de SQL, e o nome descreve exatamente o que aconteceu: o

dado foi injetado na frase e virou parte da sintaxe. A forma vulnerável é

sempre a mesma: concatenar no SQL o valor que veio de fora, como a

string no WHERE montada à mão logo abaixo, entre aspas simples. O valor e o

texto são escritos na mesma peça, e o banco não tem como saber onde o dev

acabou de falar e onde o cliente começou.

O exemplo desta aula faz a coisa errada de propósito, contra o banco de teste,

mostra o estrago, e depois faz a coisa certa com o mesmo dado.

O código vulnerável tem aspas no template

const sql = "SELECT id, nm_usuario FROM tb_usuario WHERE nm_usuario = '" + login + "'";
//                                                                                 ^ aspa que o DEV escreveu

Esse detalhe é o que o tutorial escreve e o que faz o ataque funcionar. Sem a

aspa no template, o payload clássico devolve ER_PARSE_ERROR e o código

concatenado parece seguro — foi assim que a primeira versão deste exemplo

falhou em execução.

O ataque, rodando de verdade

O login ' OR '1'='1 produz:

SELECT id, nm_usuario FROM tb_usuario WHERE nm_usuario = '' OR '1'='1'

O WHERE avaliou duas condições, não uma:

ParteQuem escreveuValor
nm_usuario = ''o DEV (o template)texto vazio, não casa com nada
OR '1'='1'o atacanteverdadeira sempre

E o SQL devolve as linhas onde qualquer condição é verdadeira: as três. O

cliente pediu um usuário e recebeu todos, sem erro e sem alerta.

Repara que o payload nem precisa de comentário: ele usa a aspa do template para

fechar o valor e abrir a condição, e a aspa do fim do código fecha a última. **O

ataque fecha sozinho.**

O mesmo código, com ?

const [linhas] = await conexao.query(
  'SELECT id, nm_usuario FROM tb_usuario WHERE nm_usuario = ?', [login]
);

O mesmo login de ataque devolve 0 linhas. O banco procurou um usuário

chamado literalmente ' OR '1'='1, e não achou — as aspas do payload viraram

texto, não sintaxe. É essa a defesa: o driver manda o valor por um caminho

separado do texto do SQL.

As três saídas que o atacante quer

ObjetivoPayloadComo funciona
Ler tudo' OR '1'='1WHERE vira sempre-verdadeiro
Apagarx; DELETE FROM t WHERE 1=1; -- ; separa comandos, -- comenta o resto
Ler senhax UNION SELECT ds_senha, nm FROM t -- UNION junta uma segunda consulta

A segunda linha é a que muda a natureza do problema: não é mais leitura indevida,

é apagar tabela por engano — o atacante vira o autor de um DELETE que o

servidor executou com a frase que ele escreveu. E é aí que a vulnerabilidade

deixa de ser um problema de consulta e vira um problema de risco: o mesmo

buraco que expõe três senhas também pode apagar a tabela inteira, e o código não

tem como limitar qual dos dois vai acontecer.

Por isso "dados expostos" é a descrição incompleta. O que está em jogo é tudo

que o usuário do banco consegue alcançar com aquele GRANT — e a defesa não é

filtrar payload no código, é não deixar o valor virar sintaxe.

O UNION é o mais caro, e tem uma regra: **as duas consultas precisam devolver o

mesmo número de colunas**. O exemplo mediu as duas formas:

As senhas dos três usuários estão na saída, vazadas pelo mesmo WHERE que o dev

escreveu para filtrar por nome. E o número de colunas do payload é o que o

atendente controla: ele lê o SELECT da aplicação e conta. Por isso SELECT *

também é um alvo.

O estrago sem volta, e o rollback

O exemplo roda a frase destrutiva de verdade, dentro de uma transação que

ele desfaz:

O ; separou os dois comandos e o -- comeu o resto da frase. Sem a transação,

esse era o estado final do banco.

Todo WHERE concatenado é o mesmo buraco

"SELECT ... WHERE nm_usuario = '" + login     // <- concatenação
"SELECT ... WHERE turma = " + turma             // <- concatenação
"SELECT ... WHERE vl_saldo > " + minimo         // <- concatenação
"SELECT ... WHERE dt BETWEEN " + ini + " AND " + fim
"SELECT ... ORDER BY " + ordem                  // <- concatenação, e o pior de todos

As cinco linhas têm a mesma forma: **o texto do SQL cresce com o valor do

cliente**. E todas as cinco aceitam o mesmo payload.

O ORDER BY é o caso mais perigoso porque o valor é um nome de coluna, e nome

de coluna não pode ser parâmetro — mudar o nome muda o plano de execução. É o

que a aula 2 de hoje resolve.

Exemplo

'use strict';

// Exemplo da aula 1 do dia 13: o que acontece quando a string do SQL e concatena.
//
// O exemplo faz a coisa ERRADA de proposito, contra o banco de teste, e mostra o
// estrago. Depois faz a coisa CERTA, com o mesmo dado, e mostra que o estrago nao
// existe. E no fim abre transacao de verdade e mostra o `DELETE` passando pelo
// `rollback` — que e o unico jeito de desfazer, e a aula 14.
//
// NADA aqui toca no banco de producao: o `.env` aponta para o banco de teste, e o
// exemplo usa tabela com nome proprio. O ataque que ele demonstra e real, e e por
// isso que ele roda contra dado descartavel.
//
// O detalhe que faz a aula funcionar: o codigo vulneravel tem as ASPAS no
// template. E o que todo tutorial de SQL injection escreve:
//
//   "SELECT ... WHERE nm_usuario = '" + login + "'"
//                                          ^ aspa que o DEV escreveu
//
// Sem essa aspa no template, o payload classico devolve `ER_PARSE_ERROR` e o
// codigo concatenao PARECE seguro. Foi assim que a primeira versao deste exemplo
// falhou em execucao.

const mysql = require('mysql2/promise');

async function main() {
  const conexao = await mysql.createConnection({
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    user: process.env.DB_USER,
    password: process.env.DB_PASS,
    database: process.env.DB_NAME,
    multipleStatements: true,
  });

  try {
    await conexao.query('DROP TABLE IF EXISTS tb_d13a1_usuario');
    await conexao.query(`
      CREATE TABLE tb_d13a1_usuario (
        id        INT AUTO_INCREMENT PRIMARY KEY,
        nm_usuario VARCHAR(30) NOT NULL,
        vl_saldo  DECIMAL(10,2) NOT NULL DEFAULT 0,
        ds_senha  VARCHAR(60) NOT NULL
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    `);
    await conexao.query(
      'INSERT INTO tb_d13a1_usuario (nm_usuario, vl_saldo, ds_senha) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)',
      ['ana', 150.00, 'hash-ana', 'bruno', 30.00, 'hash-bruno', 'carla', 0.00, 'hash-carla']
    );
    console.log('--- 3 usuarios no banco de teste ---');
    const [todos] = await conexao.query('SELECT id, nm_usuario, vl_saldo FROM tb_d13a1_usuario ORDER BY id');
    for (const u of todos) console.log('  ' + u.id + ' | ' + u.nm_usuario + ' | ' + u.vl_saldo);

    // O codigo vulneravel, exatamente como ele e escrito na internet.
    // O template esta entre aspas DUPLAS de JavaScript, e por isso o `'` da
    // aspa de SQL entra sem escape. E a forma como o codigo real e escrito, e a
    // forma que o payload classico (`' OR '1'='1`) exige para fechar sozinho.
    const SQL_VULNERAVEL = "SELECT id, nm_usuario FROM tb_d13a1_usuario WHERE nm_usuario = '";
    const sqlVuln = (login) => SQL_VULNERAVEL + login + "'";
    console.log('  (o template e uma string com aspas DUPLAS em JavaScript, e a aspa de');
    console.log('   SQL entra sem escape — e a forma que o payload classico exige)');

    // ===================================================================
    // 1. A FORMA ERRADA, com o login certo
    // ===================================================================
    console.log('');
    console.log('=== 1. A FORMA ERRADA, com o login certo ===');
    const [ok] = await conexao.query(sqlVuln('ana'));
    console.log("o SQL montado: SELECT id, nm_usuario FROM tb_d13a1_usuario WHERE nm_usuario = 'ana'");
    console.log('devolveu ' + ok.length + ' linha(s): ' + ok.map((u) => u.nm_usuario).join(', '));
    console.log('ate aqui o codigo parece certo. O erro esta no QUE vem a seguir.');

    // ===================================================================
    // 2. A FORMA ERRADA, com o login de ataque
    // ===================================================================
    console.log('');
    console.log('=== 2. O MESMO CODIGO, com o login de ataque ===');
    const ataque = "' OR '1'='1";
    console.log('o que o cliente mandou no campo de login:');
    console.log('  ' + JSON.stringify(ataque));
    console.log('o SQL que o servidor recebeu, montado pela concatenacao:');
    console.log('  ' + sqlVuln(ataque));
    console.log('');
    const [vazou] = await conexao.query(sqlVuln(ataque));
    console.log('o que voltou (' + vazou.length + ' linha(s)):');
    for (const u of vazou) console.log('  ' + u.id + ' | ' + u.nm_usuario);
    console.log('');
    console.log('O cliente pediu UM usuario e recebeu TODOS. Sem erro e sem alerta.');

    // ===================================================================
    // 3. Por que funciona: onde cada pedaco foi parar
    // ===================================================================
    //
    // A aspa do template FECHA o valor, e a primeira aspa do payload ABRE outro
    // valor. O SQL tem sintaxe, e o servidor le a frase inteira como consulta.
    console.log('');
    console.log('=== 3. onde cada pedaco foi parar ===');
    console.log('o SQL recebido, e o que o servidor leu em cada parte:');
    console.log("  WHERE nm_usuario = '" + "'" + ' OR ' + "'" + '1' + "'" + ' = ' + "'" + '1' + "'");
    console.log('  ^^^^^^^^^^^^^^^^^^^^^^ ^^ ^^^^^ ^^^ ^ ^^^^^ ^^^^^');
    console.log('   valor escrito pelo DEV     OU  valor  =  valor do atacante');
    console.log('');
    console.log('O `WHERE` avaliou DUAS condicoes, e nao uma:');
    console.log("  nm_usuario = ' OR '1'='1'  ->  o primeiro e texto, e nao casa com nada");
    console.log("  '1' = '1'                   ->  verdadeira sempre");
    console.log('E o SQL devolve as linhas onde QUALQUER condicao e verdadeira.');
    console.log('');
    console.log('Repara que o payload nem precisa de comentario: ele usa a propria aspa');
    console.log('do template para fechar o valor e abrir a condicao, e a aspa do fim do');
    console.log('codigo fecha a ultima. O ataque fecha sozinho.');

    // ===================================================================
    // 4. o mesmo codigo, com o `?`
    // ===================================================================
    console.log('');
    console.log('=== 4. o MESMO codigo, com o `?` ===');
    const [protegido] = await conexao.query(
      'SELECT id, nm_usuario FROM tb_d13a1_usuario WHERE nm_usuario = ?', [ataque]
    );
    console.log('o mesmo login de ataque, agora como PARAMETRO:');
    console.log('  devolveu ' + protegido.length + ' linha(s)');
    console.log('  o banco procurou um usuario chamado literalmente ' + JSON.stringify(ataque) + ',');
    console.log('  e nao achou. As aspas do payload viraram TEXTO, e nao sintaxe.');

    // ===================================================================
    // 5. as tres saidas do ataque
    // ===================================================================
    console.log('');
    console.log('=== 5. as tres saidas que o atacante quer ===');
    console.log("1. LER TUDO   | login: ' OR '1'='1");
    console.log('                o WHERE vira sempre-verdadeiro e devolve a tabela inteira');
    console.log('');
    console.log('2. APAGAR     | login: x; DELETE FROM tb_d13a1_usuario WHERE 1=1; -- ');
    console.log('                o ponto-e-virgula separa comandos e o -- comenta o resto');
    console.log('');
    console.log('3. LER SENHA  | login: x UNION SELECT ds_senha, nm_usuario FROM tb_d13a1_usuario -- ');
    console.log('                o UNION junta uma segunda consulta, com as colunas que o');
    console.log('                atacante escolher');
    console.log('');
    console.log('O `UNION` e o mais caro, e ele tem uma regra: as duas consultas precisam');
    console.log('devolver o MESMO numero de colunas. Com a consulta devolvendo 2, o payload');
    console.log('precisa ter 2:');
    let umColuna;
    try {
      const [r] = await conexao.query(sqlVuln("' UNION SELECT ds_senha FROM tb_d13a1_usuario -- "));
      umColuna = 'devolveu ' + r.length + ' linha(s)';
    } catch (erro) {
      umColuna = 'recusado, code ' + erro.code;
    }
    console.log('  payload com 1 coluna  -> ' + umColuna);
    const [comColuna] = await conexao.query(
      sqlVuln("' UNION SELECT ds_senha, nm_usuario FROM tb_d13a1_usuario -- ")
    );
    console.log('  payload com 2 colunas -> devolveu ' + comColuna.length + ' linha(s)');
    for (const l of comColuna) {
      console.log('    ' + l.id + ' | ' + l.nm_usuario + '  <- a coluna primeira agora e a SENHA');
    }
    console.log('  as senhas dos tres usuarios estao ACIMA, vazadas pelo mesmo `WHERE` que');
    console.log('  o dev escreveu para filtrar por nome.');
    console.log('  E o numero de colunas do payload e o que o atacante controla: ele le o');
    console.log('  `SELECT` da aplicacao e conta. Por isso `SELECT *` tambem e um alvo.');

    // ===================================================================
    // 6. o estrago que nao tem volta, e o rollback
    // ===================================================================
    //
    // Aqui a frase destrutiva roda DE VERDADE, dentro de uma transacao que o
    // exemplo desfaz. E a prova de que o estrago da secao 5 existe: e a unica
    // forma de mostrar sem destruir o banco.
    console.log('');
    console.log('=== 6. o DELETE de verdade, dentro de uma transacao desfeita ===');
    const [antes] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d13a1_usuario');
    console.log('usuarios antes:', antes[0].n);
    await conexao.beginTransaction();
    try {
      await conexao.query(
        "SELECT id FROM tb_d13a1_usuario WHERE nm_usuario = 'x'; DELETE FROM tb_d13a1_usuario WHERE 1=1; -- x"
      );
      const [dentro] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d13a1_usuario');
      console.log('usuarios DENTRO da transacao:', dentro[0].n, '<-- apagou de verdade, agora');
      console.log('o `;` separou os dois comandos e o `--` comeu o resto da frase.');
      console.log('vou fazer rollback, e o que importa e o que acontece DEPOIS:');
      await conexao.rollback();
    } catch (erro) {
      await conexao.rollback();
      console.error('  ' + erro.code + ': ' + erro.message);
      console.log('  o banco recusou a frase:', erro.code);
    }
    const [depois] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d13a1_usuario');
    console.log('usuarios depois do rollback:', depois[0].n, '<-- voltou tudo');
    console.log('');
    console.log('O `rollback` devolve a tabela ao estado anterior. E a defesa alem da');
    console.log('parametrizacao — e por isso que a aula 14 existe: transacao nao e sintaxe');
    console.log('bonita, e o que impede que o estrago vire permanente.');

    // ===================================================================
    // 7. todo WHERE concatenao e o mesmo buraco
    // ===================================================================
    console.log('');
    //
    // Cada item abaixo e o TEXTO de uma linha de codigo vulneravel, escrito
    // dentro de crase para poder mostrar a concatenacao sem executá-la de novo.
    const exemplosVuln = [
      `  "SELECT ... WHERE nm_usuario = ' + login"`,
      `  "SELECT ... WHERE turma = " + turma`,
      `  "SELECT ... WHERE vl_saldo > " + minimo`,
      `  "SELECT ... WHERE dt BETWEEN " + ini + " AND " + fim`,
      `  "SELECT ... ORDER BY " + ordem`,
    ];
    for (const linha of exemplosVuln) {
      console.log(linha.padEnd(48) + ' <- concatenao');
    }
    console.log('');
    console.log('as cinco linhas acima tem a mesma forma: o TEXTO do SQL cresce com o');
    console.log('valor do cliente. E todas as cinco aceitam o mesmo payload.');
    console.log('');
    console.log('O `ORDER BY` e o caso mais perigoso porque o valor e um NOME DE COLUNA, e');
    console.log('nome de coluna nao pode ser parametro. A aula 2 de hoje mostra a saida.');
  } finally {
    await conexao.query('DROP TABLE IF EXISTS tb_d13a1_usuario');
    await conexao.end();
    console.log('');
    console.log('tabela de teste removida; conexao encerrada com end().');
  }
}

main().catch((erro) => {
  console.error('falhou:', erro.code || erro.name, '-', erro.message);
  process.exit(1);
});

Saída real

--- 3 usuarios no banco de teste ---
  1 | ana | 150.00
  2 | bruno | 30.00
  3 | carla | 0.00
  (o template e uma string com aspas DUPLAS em JavaScript, e a aspa de
   SQL entra sem escape — e a forma que o payload classico exige)

=== 1. A FORMA ERRADA, com o login certo ===
o SQL montado: SELECT id, nm_usuario FROM tb_d13a1_usuario WHERE nm_usuario = 'ana'
devolveu 1 linha(s): ana
ate aqui o codigo parece certo. O erro esta no QUE vem a seguir.

=== 2. O MESMO CODIGO, com o login de ataque ===
o que o cliente mandou no campo de login:
  "' OR '1'='1"
o SQL que o servidor recebeu, montado pela concatenacao:
  SELECT id, nm_usuario FROM tb_d13a1_usuario WHERE nm_usuario = '' OR '1'='1'

o que voltou (3 linha(s)):
  1 | ana
  2 | bruno
  3 | carla

O cliente pediu UM usuario e recebeu TODOS. Sem erro e sem alerta.

=== 3. onde cada pedaco foi parar ===
o SQL recebido, e o que o servidor leu em cada parte:
  WHERE nm_usuario = '' OR '1' = '1'
  ^^^^^^^^^^^^^^^^^^^^^^ ^^ ^^^^^ ^^^ ^ ^^^^^ ^^^^^
   valor escrito pelo DEV     OU  valor  =  valor do atacante

O `WHERE` avaliou DUAS condicoes, e nao uma:
  nm_usuario = ' OR '1'='1'  ->  o primeiro e texto, e nao casa com nada
  '1' = '1'                   ->  verdadeira sempre
E o SQL devolve as linhas onde QUALQUER condicao e verdadeira.

Repara que o payload nem precisa de comentario: ele usa a propria aspa
do template para fechar o valor e abrir a condicao, e a aspa do fim do
codigo fecha a ultima. O ataque fecha sozinho.

=== 4. o MESMO codigo, com o `?` ===
o mesmo login de ataque, agora como PARAMETRO:
  devolveu 0 linha(s)
  o banco procurou um usuario chamado literalmente "' OR '1'='1",
  e nao achou. As aspas do payload viraram TEXTO, e nao sintaxe.

=== 5. as tres saidas que o atacante quer ===
1. LER TUDO   | login: ' OR '1'='1
                o WHERE vira sempre-verdadeiro e devolve a tabela inteira

2. APAGAR     | login: x; DELETE FROM tb_d13a1_usuario WHERE 1=1; -- 
                o ponto-e-virgula separa comandos e o -- comenta o resto

3. LER SENHA  | login: x UNION SELECT ds_senha, nm_usuario FROM tb_d13a1_usuario -- 
                o UNION junta uma segunda consulta, com as colunas que o
                atacante escolher

O `UNION` e o mais caro, e ele tem uma regra: as duas consultas precisam
devolver o MESMO numero de colunas. Com a consulta devolvendo 2, o payload
precisa ter 2:
  payload com 1 coluna  -> recusado, code ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT
  payload com 2 colunas -> devolveu 3 linha(s)
    hash-ana | ana  <- a coluna primeira agora e a SENHA
    hash-bruno | bruno  <- a coluna primeira agora e a SENHA
    hash-carla | carla  <- a coluna primeira agora e a SENHA
  as senhas dos tres usuarios estao ACIMA, vazadas pelo mesmo `WHERE` que
  o dev escreveu para filtrar por nome.
  E o numero de colunas do payload e o que o atacante controla: ele le o
  `SELECT` da aplicacao e conta. Por isso `SELECT *` tambem e um alvo.

=== 6. o DELETE de verdade, dentro de uma transacao desfeita ===
usuarios antes: 3
usuarios DENTRO da transacao: 0 <-- apagou de verdade, agora
o `;` separou os dois comandos e o `--` comeu o resto da frase.
vou fazer rollback, e o que importa e o que acontece DEPOIS:
usuarios depois do rollback: 3 <-- voltou tudo

O `rollback` devolve a tabela ao estado anterior. E a defesa alem da
parametrizacao — e por isso que a aula 14 existe: transacao nao e sintaxe
bonita, e o que impede que o estrago vire permanente.

  "SELECT ... WHERE nm_usuario = ' + login"      <- concatenao
  "SELECT ... WHERE turma = " + turma            <- concatenao
  "SELECT ... WHERE vl_saldo > " + minimo        <- concatenao
  "SELECT ... WHERE dt BETWEEN " + ini + " AND " + fim <- concatenao
  "SELECT ... ORDER BY " + ordem                 <- concatenao

as cinco linhas acima tem a mesma forma: o TEXTO do SQL cresce com o
valor do cliente. E todas as cinco aceitam o mesmo payload.

O `ORDER BY` e o caso mais perigoso porque o valor e um NOME DE COLUNA, e
nome de coluna nao pode ser parametro. A aula 2 de hoje mostra a saida.

tabela de teste removida; conexao encerrada com end().
Aula 2

Consultas parametrizadas

A parte que o ? não cobre

O ? resolve valor. Ele não resolve nome de coluna, nome de tabela, direção

do ORDER BY, nem curinga de LIKE. E não é falha do driver.

O que o ? realmente protege

A forma certa se chama consulta parametrizada, e a peça que a faz é o ? — o

placeholder, o sinal de interrogação que fica no texto do SQL no lugar do valor.

Executar com parâmetro é passar o valor no segundo argumento, separado do texto:

const [linhas] = await conexao.query(
  'SELECT id, nm_usuario FROM tb_usuario WHERE nm_usuario = ?', [login]
);

O driver monta um prepared statement: ele manda o texto da consulta ao servidor,

o servidor prepara o plano, e o valor viaja na mensagem seguinte — é o que se

chama bind parameter, o parâmetro que se liga ao ? já preparado. O servidor

nunca lê o valor como texto da frase, e é por isso que a segurança no SQL vem de

graça: não é filtro, é separação de caminho.

A regra que decorre: sempre parametrizar, e não concatenar. Nunca escrever o

valor dentro do texto do SQL — nem escapado à mão, nem "só neste caso".

Medido neste servidor (MariaDB 10.11 + mysql2):

O ? é aceito na posição de coluna. Ele ocupa uma posição de expressão,

e o nome de uma coluna é uma expressão válida ali. Não é que o driver "proíba" —

é que o ? viaja por um caminho separado do texto, e nome de coluna é um valor

legítimo nessa posição.

E é exatamente por isso que o ? não resolve o problema. Ele protege contra

o conteúdo: um ; DROP TABLE vira texto e quebra a consulta, sem executar

nada. Mas um ? numa posição de coluna aceita qualquer expressão —

? com o valor 1 DESC, (SELECT ...) muda a ordem. O driver não interpreta o

texto; o servidor sim. O ? segura valor de dado, não de sintaxe.

A concatenação: o resultado imprevisível

Existe uma função que parece resolver: mysql.escape(valor). Ela devolve o valor

como texto com aspas e escapes corretamente escapados, e é a assinatura dela que

convence — parece que o problema foi tratado.

Não foi. O escape continua sendo concatenação: o texto do SQL cresce com o

valor do cliente, e o que muda é só quantas barras aparecem no resultado. Ele

protege o caso comum e não protege o resto — e o resto é justamente o que o

? resolve por construção. Onde o driver tem uma forma que separa os dois

caminhos, usar escape é aceitar uma forma pior em troca de nada.

O cliente manda id; DROP TABLE tb_d13a2_produto; -- no parâmetro de ordenação.

O SQL concatenado fica:

SELECT nm_produto, vl_preco FROM tb_d13a2_produto ORDER BY id; DROP TABLE tb_d13a2_produto; --

O banco devolve ER_PARSE_ERROR. A frase parou no id, que é uma coluna válida;

o resto virou cauda de um item de lista do ORDER BY, e o banco leu `; DROP

TABLE ...` como expressões malformadas.

Nada foi apagado — e é esse o problema. O mesmo payload pode devolver 500,

devolver a lista em ordem errada, ou — com uma palavra a menos, ou com

multipleStatements ligado na outra ponta — apagar a tabela. O código não tem

como saber qual dos três vai acontecer, e não é teste que descobre.

A lista fechada: o código escolhe a coluna

const COLUNAS = {
  preco: 'vl_preco',
  nome: 'nm_produto',
  categoria: 'nm_categoria',
  estoque: 'qtde',
};

const colunaPara = (opcao) => (Object.hasOwn(COLUNAS, opcao) ? COLUNAS[opcao] : COLUNAS.preco);

const [linhas] = await conexao.query(
  'SELECT nm_produto, vl_preco FROM tb_d13a2_produto ORDER BY ' + colunaPara(pedido) + ' ASC LIMIT 3'
);

O + continua ali. A diferença é o que ele concatena: não é mais o valor do

cliente, é um nome que o dev escreveu. O cliente pediu "preco" e o código

decidiu qual coluna é essa.

pedido do clientecoluna escolhida
precovl_preco
nomenm_produto
categorianm_categoria
id; DROP TABLE ...; -- vl_preco (o padrão)

O mesmo pedido de ataque da seção 2, pelo código com lista fechada, devolveu

3 linhas e a tabela continuou de pé com os 5 produtos.

Object.hasOwn fecha a porta lateral

COLUNAS[pedido] sozinho não basta. As chaves __proto__ e constructor

existem no prototype, então passariam pela busca sem nunca estarem no objeto.

O pedido viraria um objeto ou uma função no meio do SQL, e o ORDER BY

receberia undefined — um erro diferente, mais difícil de ler, e igual de

concreto. Por isso o Object.hasOwn e não um pedido in COLUNAS.

A direção: mesma regra

const DIRECOES = { asc: 'ASC', desc: 'DESC' };
const direcaoPara = (d) => (Object.hasOwn(DIRECOES, d) ? DIRECOES[d] : DIRECOES.asc);
pedidodireçãoprimeiro preço
ascASC60.00
descDESC1450.00
DESC; SELECT 1ASC60.00
qualquer outra coisaASC60.00

LIKE: o curinga que o cliente escreve

% e _ são curingas do LIKE. Um parâmetro de busca que vai direto para o

LIKE deixa o cliente escrever o curinga:

buscalinhaso que aconteceu
cadeira2o texto exato
c%3o cliente mandou o curinga — achou teclado
%adeira2o curinga no início
%5só o curinga: lista tudo

Isso é o LIKE funcionando, não uma falha: ninguém foi invadido, o cliente só

pediu um filtro mais largo do que imaginava.

Quando precisa ser impedido, o caminho é o ESCAPE, que declara qual caractere

é o curinga e trata o resto como texto:

const [linhas] = await conexao.query(
  "SELECT nm_produto FROM tb_d13a2_produto WHERE nm_produto LIKE ? ESCAPE '!'",
  ['%c!%adeira%']
);

O ! antes do % diz "o próximo caractere é literal" — e o exemplo mediu **0

linhas**: as duas cadeiras sumiram, porque nenhuma tem um % no meio do nome. O

ESCAPE faz parte do SQL, escrito pelo dev, não pelo cliente.

Exemplo

'use strict';

// Exemplo da aula 2 do dia 13: como escrever a parte que o `?` nao cobre.
//
// O `?` resolve valor. Ele NAO resolve nome de coluna, nome de tabela, direcao
// do ORDER BY, nem palavra de LIKE. Nao e falha do driver: e que o `?` viaja
// por um caminho separado do texto, e nome de coluna mora no TEXTO do SQL — se
// o `?` fosse aceito ali, o plano de execucao mudaria a cada linha.
//
// A solucao nesses casos nunca e escapar o valor. E uma LISTA FECHADA: o codigo
// mapeia o que o cliente pediu para um nome que o DEV escreveu.

const mysql = require('mysql2/promise');

async function main() {
  const conexao = await mysql.createConnection({
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    user: process.env.DB_USER,
    password: process.env.DB_PASS,
    database: process.env.DB_NAME,
  });

  try {
    await conexao.query('DROP TABLE IF EXISTS tb_d13a2_produto');
    await conexao.query(`
      CREATE TABLE tb_d13a2_produto (
        id          INT AUTO_INCREMENT PRIMARY KEY,
        nm_produto  VARCHAR(40) NOT NULL,
        nm_categoria VARCHAR(20) NOT NULL,
        vl_preco    DECIMAL(10,2) NOT NULL,
        qtde        INT NOT NULL DEFAULT 0
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    `);
    await conexao.query(
      'INSERT INTO tb_d13a2_produto (nm_produto, nm_categoria, vl_preco, qtde) VALUES (?,?,?,?), (?,?,?,?), (?,?,?,?), (?,?,?,?), (?,?,?,?)',
      ['teclado', 'periferico', 150.00, 12, 'mouse', 'periferico', 60.00, 30, 'cadeira', 'mobiliario', 620.00, 5, 'monitor', 'periferico', 890.00, 8, 'cadeira gamer', 'mobiliario', 1450.00, 3]
    );
    console.log('--- 5 produtos de teste ---');
    const [p0] = await conexao.query('SELECT nm_produto, nm_categoria, vl_preco FROM tb_d13a2_produto ORDER BY id');
    for (const p of p0) console.log('  ' + p.nm_produto.padEnd(14) + ' | ' + p.nm_categoria.padEnd(11) + ' | ' + p.vl_preco);

    // ===================================================================
    // 1. Por que o `?` nao aceita nome de coluna
    // ===================================================================
    console.log('');
    console.log('=== 1. por que o `?` NAO aceita nome de coluna ===');
    const sqlOrdem = 'SELECT nm_produto, vl_preco FROM tb_d13a2_produto ORDER BY ? LIMIT 2';
    try {
      const [rk] = await conexao.query(sqlOrdem, ['vl_preco']);
      console.log('  ORDER BY ? , [ "vl_preco" ]  -> ACEITO, devolveu ' + rk.length + ' linha(s):');
      for (const k of rk) console.log('      ' + k.nm_produto + '  ' + k.vl_preco);
    } catch (erro) {
      console.log('  ORDER BY ?  -> recusado, code ' + erro.code);
    }
    try {
      await conexao.query('SELECT id FROM tb_d13a2_produto WHERE ? = ?', ['vl_preco', 150]);
      console.log('  WHERE ? = ? , [ "vl_preco", 150 ] -> ACEITO');
    } catch (erro) {
      console.log('  WHERE ? = ?  -> recusado, code ' + erro.code);
      console.log('  ' + erro.message.split('\n')[0]);
    }
    console.log('');
    console.log('MEDIDO NESTE SERVIDOR: o `?` no `ORDER BY` e no `WHERE` e ACEITO, e');
    console.log('funciona — o `?` vira um item de expressao, como uma coluna ali. Nao e');
    console.log('que o driver "proiba": e que o `?` ocupa uma posicao de VALOR, e o nome');
    console.log('de uma coluna e um valor valido nessa posicao.');
    console.log('');
    console.log('E e exatamente por isso que o `?` NAO resolve o problema. O `?` protege');
    console.log('contra o CONTEUDO: um `; DROP TABLE` vira texto e quebra a consulta, sem');
    console.log('executar nada. Mas um `?` na posicao de coluna aceita QUALQUER');
    console.log('EXPRESSAO: `?` com o valor `1 DESC, (SELECT ...)` muda a ordem. O driver');
    console.log('nao vai interpretar o texto, e o servidor vai. O `?` segura valor de');
    console.log('DADO, e nao valor de SINTAXE — entao coluna e direcao continuam');
    console.log('precisando da lista fechada, que e a secao 3.');

    // ===================================================================
    // 2. a concatenacao com nome de coluna: o estrago real
    // ===================================================================
    console.log('');
    console.log('=== 2. a concatenacao com nome de coluna ===');
    const entrada = 'id; DROP TABLE tb_d13a2_produto; -- ';
    console.log('o cliente mandou no parametro de ordenacao:');
    console.log('  ' + JSON.stringify(entrada));
    console.log('');
    console.log('o SQL que sairia da concatenacao:');
    console.log('  SELECT ... FROM tb_d13a2_produto ORDER BY ' + entrada);
    console.log('');
    // O codigo concatenado de verdade, com o mesmo `?` de driver que a secao 1.
    const sqlCat = 'SELECT nm_produto, vl_preco FROM tb_d13a2_produto ORDER BY ' + entrada;
    console.log('o SQL concatenado, pronto para o servidor:');
    console.log('  ' + sqlCat);
    console.log('');
    try {
      const [r] = await conexao.query(sqlCat);
      console.log('devolveu ' + r.length + ' linha(s) SEM ERRO.');
    } catch (erro) {
      console.log('recusado, code ' + erro.code);
      console.log('  ' + erro.message.split('\n')[0]);
    }
    console.log('');
    console.log('O ponto e o silencio do `ER_PARSE_ERROR`. A frase parou no `id`, que e');
    console.log('uma coluna valida: o resto virou cauda de um item de lista do ORDER');
    console.log('BY, e o banco leu `; DROP TABLE ...` como expressoes malformadas. Nada');
    console.log('foi apagado, e o cliente recebeu um erro 500 em vez de uma lista.');
    console.log('');
    console.log('Isto e o que torna a concatenacao perigosa na pratica: o resultado');
    console.log('imprevisivel. O mesmo payload pode devolver 500, devolver a lista em');
    console.log('ordem errada, ou — com uma palavra a menos, ou com `multipleStatements`');
    console.log('ligado na outra ponta — apagar a tabela. O codigo nao tem como saber');
    console.log('qual dos tres vai acontecer, e nao e teste que descobre.');

    // ===================================================================
    // 3. a LISTA FECHADA: o mapeamento que resolve
    // ===================================================================
    //
    // O codigo abaixo NAO recebe nome de coluna. Ele recebe uma OPCAO ("preco"),
    // e escolhe sozinho qual nome de coluna usar. O cliente pode pedir o que
    // quiser: se nao estiver na lista, cai no padrao.
    console.log('');
    console.log('=== 3. a LISTA FECHADA: o codigo escolhe a coluna ===');
    const COLUNAS = {
      preco: 'vl_preco',
      nome: 'nm_produto',
      categoria: 'nm_categoria',
      estoque: 'qtde',
    };
    // `Object.hasOwn` impede que o cliente use `__proto__` ou `constructor`
    // para sair da lista: o prototype herdado tambem tem chaves.
    const colunaPara = (opcao) => (Object.hasOwn(COLUNAS, opcao) ? COLUNAS[opcao] : COLUNAS.preco);
    console.log('o codigo declare, e o DEV que escreve:');
    console.log('  preco      -> vl_preco');
    console.log('  nome       -> nm_produto');
    console.log('  categoria  -> nm_categoria');
    console.log('  estoque    -> qtde');
    console.log('');
    for (const pedido of ['preco', 'nome', 'categoria', 'estoque']) {
      const [r] = await conexao.query(
        'SELECT nm_produto, vl_preco FROM tb_d13a2_produto ORDER BY ' + colunaPara(pedido) + ' ASC LIMIT 3'
      );
      const primeiro = r.length > 0 ? r[0].nm_produto : '(vazio)';
      console.log("  pedido '" + pedido + "' -> coluna '" + colunaPara(pedido) + "' -> primeiro: " + primeiro);
    }
    console.log('');
    console.log('o `+` ainda esta ali. A diferenca e o que ele concatena: nao e mais o');
    console.log('valor do cliente, e um nome que o DEV escreveu. O cliente pediu "preco" e');
    console.log('o codigo decidiu qual coluna e essa.');

    // ===================================================================
    // 4. o mesmo pedido, com o payload de ataque
    // ===================================================================
    console.log('');
    console.log('=== 4. o MESMO codigo, com o payload de ataque ===');
    const ataque2 = 'id; DROP TABLE tb_d13a2_produto; -- ';
    const [r2] = await conexao.query(
      'SELECT nm_produto, vl_preco FROM tb_d13a2_produto ORDER BY ' + colunaPara(ataque2) + ' ASC LIMIT 3'
    );
    console.log("o cliente pediu: " + JSON.stringify(ataque2));
    console.log("o codigo traduziu para a coluna: '" + colunaPara(ataque2) + "'  <- o padrao");
    console.log('a consulta rodou e devolveu ' + r2.length + ' linha(s). A tabela continua de pe.');
    const [contagem] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d13a2_produto');
    console.log('a tabela ainda tem ' + contagem[0].n + ' produto(s). O `DROP TABLE` virou');
    console.log('texto de um nome de coluna que o codigo nunca usou.');
    console.log('');
    console.log('E o `Object.hasOwn` e o que fecha a porta lateral do `COLUNAS[pedido]`:');
    console.log('sem ele, o pedido `__proto__` ou `constructor` passaria pela busca,');
    console.log('porque essas chaves EXISTEM no prototype. O pedido viraria um objeto ou');
    console.log('uma funcao no meio do SQL, e o `ORDER BY` receberia `undefined` — um');
    console.log('erro diferente, mais dificil de ler, e igual de concreto.');

    // ===================================================================
    // 5. a mesma logica para a DIRECAO
    // ===================================================================
    console.log('');
    console.log('=== 5. e para a direcao do ORDER BY ===');
    const DIRECOES = { asc: 'ASC', desc: 'DESC' };
    const direcaoPara = (d) => (Object.hasOwn(DIRECOES, d) ? DIRECOES[d] : DIRECOES.asc);
    console.log("o codigo aceita 'asc' ou 'desc', e traduz para:");
    console.log("  asc  -> ASC");
    console.log("  desc -> DESC");
    console.log('');
    for (const pedido of ['asc', 'desc', 'DESC; SELECT 1', 'qualquer outra coisa']) {
      const [r] = await conexao.query(
        'SELECT nm_produto, vl_preco FROM tb_d13a2_produto ORDER BY vl_preco ' + direcaoPara(pedido) + ' LIMIT 2'
      );
      console.log("  pedido " + JSON.stringify(pedido).padEnd(24) + ' -> ' + direcaoPara(pedido).padEnd(5) + ' -> ' + r.map((x) => x.vl_preco).join(', '));
    }
    console.log('');
    console.log('Um `DESC` com ponto e virgula dentro devolveria `ER_PARSE_ERROR` na');
    console.log('forma concatenada. Aqui virou `ASC`, e a consulta rodou.');

    // ===================================================================
    // 6. `LIKE` com `%` do cliente
    // ===================================================================
    //
    // `LIKE` aceita `%` e `_` como curinga. Um parametro de busca que vai direto
    // pro LIKE deixa o cliente escrever o curinga — e isso NAO e seguranca, e
    // esperado. O que se faz e escapar o proprio curinga com `ESCAPE`.
    console.log('');
    console.log('=== 6. `LIKE` e o curinga que o cliente escreve ===');
    //
    // O teste que separa os dois: o `%` e um curinga, o `a` nao. A busca `cadeira`
    // deve achar duas linhas; a busca `c%` deve achar TRES (cadeira, cadeira
    // gamer, e o que mais comecar com c), e a busca `cadeira%` tambem tres, porque
    // o `%` casa o resto.
    const casos = [
      ['cadeira',  'sem curinga, o texto exato'],
      ['c%',       'o cliente mandou o curinga'],
      ['%adeira',  'o curinga no inicio'],
      ['%',        'so o curinga: lista tudo'],
    ];
    console.log('cada linha: o que o cliente mandou, e o que o LIKE devolveu');
    console.log('');
    for (const [texto, nota] of casos) {
      const [c] = await conexao.query('SELECT nm_produto FROM tb_d13a2_produto WHERE nm_produto LIKE ?', ['%' + texto + '%']);
      console.log('  ' + texto.padEnd(10) + ' -> ' + String(c.length).padStart(2) + ' linha(s)   ' + nota);
      for (const x of c) console.log('        ' + x.nm_produto);
    }
    console.log('');
    console.log('O `%` do cliente virou "qualquer sequencia". A busca `c%` devolveu');
    console.log('`cadeira gamer` e `cadeira` — linhas que o texto `c` sozinho nao');
    console.log('acharia. Isso e o LIKE funcionando, e nao uma falha: ninguem foi');
    console.log('invadido, o cliente so pediu um filtro mais largo do que imaginava.');
    console.log('');
    console.log('Quando isso PRECISA ser impedido, o caminho e o `ESCAPE`, que diz ao');
    console.log('qual caractere e o curinga e trata os outros como texto:');
    const [esc] = await conexao.query(
      "SELECT nm_produto FROM tb_d13a2_produto WHERE nm_produto LIKE ? ESCAPE '!'",
      ['%c!%adeira%']
    );
    console.log("  LIKE '%c!%adeira%' ESCAPE '!'  -> " + esc.length + ' linha(s)');
    for (const x of esc) console.log('        ' + x.nm_produto);
    console.log('');
    console.log('o caractere de escape fica logo ANTES do `%`, e o `ESCAPE` diz qual');
    console.log('e. O `c!%adeira` procurou a palavra inteira: as duas cadeiras acima');
    console.log('sumiram, porque nenhuma tem um `%` no meio do nome. O `ESCAPE` e o');
    console.log('unico jeito de dizer "neste LIKE, o curinga e este" — e ele faz parte');
    console.log('do SQL, escrito pelo DEV, nao pelo cliente.');
  } finally {
    await conexao.query('DROP TABLE IF EXISTS tb_d13a2_produto');
    await conexao.end();
    console.log('');
    console.log('tabela de teste removida; conexao encerrada com end().');
  }
}

main().catch((erro) => {
  console.error('falhou:', erro.code || erro.name, '-', erro.message);
  process.exit(1);
});

Saída real

--- 5 produtos de teste ---
  teclado        | periferico  | 150.00
  mouse          | periferico  | 60.00
  cadeira        | mobiliario  | 620.00
  monitor        | periferico  | 890.00
  cadeira gamer  | mobiliario  | 1450.00

=== 1. por que o `?` NAO aceita nome de coluna ===
  ORDER BY ? , [ "vl_preco" ]  -> ACEITO, devolveu 2 linha(s):
      teclado  150.00
      mouse  60.00
  WHERE ? = ? , [ "vl_preco", 150 ] -> ACEITO

MEDIDO NESTE SERVIDOR: o `?` no `ORDER BY` e no `WHERE` e ACEITO, e
funciona — o `?` vira um item de expressao, como uma coluna ali. Nao e
que o driver "proiba": e que o `?` ocupa uma posicao de VALOR, e o nome
de uma coluna e um valor valido nessa posicao.

E e exatamente por isso que o `?` NAO resolve o problema. O `?` protege
contra o CONTEUDO: um `; DROP TABLE` vira texto e quebra a consulta, sem
executar nada. Mas um `?` na posicao de coluna aceita QUALQUER
EXPRESSAO: `?` com o valor `1 DESC, (SELECT ...)` muda a ordem. O driver
nao vai interpretar o texto, e o servidor vai. O `?` segura valor de
DADO, e nao valor de SINTAXE — entao coluna e direcao continuam
precisando da lista fechada, que e a secao 3.

=== 2. a concatenacao com nome de coluna ===
o cliente mandou no parametro de ordenacao:
  "id; DROP TABLE tb_d13a2_produto; -- "

o SQL que sairia da concatenacao:
  SELECT ... FROM tb_d13a2_produto ORDER BY id; DROP TABLE tb_d13a2_produto; -- 

o SQL concatenado, pronto para o servidor:
  SELECT nm_produto, vl_preco FROM tb_d13a2_produto ORDER BY id; DROP TABLE tb_d13a2_produto; -- 

recusado, code ER_PARSE_ERROR
  You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'DROP TABLE tb_d13a2_produto; --' at line 1

O ponto e o silencio do `ER_PARSE_ERROR`. A frase parou no `id`, que e
uma coluna valida: o resto virou cauda de um item de lista do ORDER
BY, e o banco leu `; DROP TABLE ...` como expressoes malformadas. Nada
foi apagado, e o cliente recebeu um erro 500 em vez de uma lista.

Isto e o que torna a concatenacao perigosa na pratica: o resultado
imprevisivel. O mesmo payload pode devolver 500, devolver a lista em
ordem errada, ou — com uma palavra a menos, ou com `multipleStatements`
ligado na outra ponta — apagar a tabela. O codigo nao tem como saber
qual dos tres vai acontecer, e nao e teste que descobre.

=== 3. a LISTA FECHADA: o codigo escolhe a coluna ===
o codigo declare, e o DEV que escreve:
  preco      -> vl_preco
  nome       -> nm_produto
  categoria  -> nm_categoria
  estoque    -> qtde

  pedido 'preco' -> coluna 'vl_preco' -> primeiro: mouse
  pedido 'nome' -> coluna 'nm_produto' -> primeiro: cadeira
  pedido 'categoria' -> coluna 'nm_categoria' -> primeiro: cadeira gamer
  pedido 'estoque' -> coluna 'qtde' -> primeiro: cadeira gamer

o `+` ainda esta ali. A diferenca e o que ele concatena: nao e mais o
valor do cliente, e um nome que o DEV escreveu. O cliente pediu "preco" e
o codigo decidiu qual coluna e essa.

=== 4. o MESMO codigo, com o payload de ataque ===
o cliente pediu: "id; DROP TABLE tb_d13a2_produto; -- "
o codigo traduziu para a coluna: 'vl_preco'  <- o padrao
a consulta rodou e devolveu 3 linha(s). A tabela continua de pe.
a tabela ainda tem 5 produto(s). O `DROP TABLE` virou
texto de um nome de coluna que o codigo nunca usou.

E o `Object.hasOwn` e o que fecha a porta lateral do `COLUNAS[pedido]`:
sem ele, o pedido `__proto__` ou `constructor` passaria pela busca,
porque essas chaves EXISTEM no prototype. O pedido viraria um objeto ou
uma funcao no meio do SQL, e o `ORDER BY` receberia `undefined` — um
erro diferente, mais dificil de ler, e igual de concreto.

=== 5. e para a direcao do ORDER BY ===
o codigo aceita 'asc' ou 'desc', e traduz para:
  asc  -> ASC
  desc -> DESC

  pedido "asc"                    -> ASC   -> 60.00, 150.00
  pedido "desc"                   -> DESC  -> 1450.00, 890.00
  pedido "DESC; SELECT 1"         -> ASC   -> 60.00, 150.00
  pedido "qualquer outra coisa"   -> ASC   -> 60.00, 150.00

Um `DESC` com ponto e virgula dentro devolveria `ER_PARSE_ERROR` na
forma concatenada. Aqui virou `ASC`, e a consulta rodou.

=== 6. `LIKE` e o curinga que o cliente escreve ===
cada linha: o que o cliente mandou, e o que o LIKE devolveu

  cadeira    ->  2 linha(s)   sem curinga, o texto exato
        cadeira
        cadeira gamer
  c%         ->  3 linha(s)   o cliente mandou o curinga
        teclado
        cadeira
        cadeira gamer
  %adeira    ->  2 linha(s)   o curinga no inicio
        cadeira
        cadeira gamer
  %          ->  5 linha(s)   so o curinga: lista tudo
        teclado
        mouse
        cadeira
        monitor
        cadeira gamer

O `%` do cliente virou "qualquer sequencia". A busca `c%` devolveu
`cadeira gamer` e `cadeira` — linhas que o texto `c` sozinho nao
acharia. Isso e o LIKE funcionando, e nao uma falha: ninguem foi
invadido, o cliente so pediu um filtro mais largo do que imaginava.

Quando isso PRECISA ser impedido, o caminho e o `ESCAPE`, que diz ao
qual caractere e o curinga e trata os outros como texto:
  LIKE '%c!%adeira%' ESCAPE '!'  -> 0 linha(s)

o caractere de escape fica logo ANTES do `%`, e o `ESCAPE` diz qual
e. O `c!%adeira` procurou a palavra inteira: as duas cadeiras acima
sumiram, porque nenhuma tem um `%` no meio do nome. O `ESCAPE` e o
unico jeito de dizer "neste LIKE, o curinga e este" — e ele faz parte
do SQL, escrito pelo DEV, nao pelo cliente.

tabela de teste removida; conexao encerrada com end().