Sistemas legados em PHP com Firebird compartilham um padrão perigoso: montar SQL juntando strings. Funciona por anos, até o dia em que um parâmetro vem da URL e o banco devolve dados que não deveria.
O problema não é a idade do código nem a falta de framework. É que a fronteira entre comando e dado desaparece quando você concatena. Neste artigo, o ataque é reproduzido, a causa é explicada e a correção é mostrada com o driver ibase.
O cenário real: um parâmetro na URL
Um endpoint público recebe um identificador na query string e busca o token correspondente:
$id = trim($_GET['identificador']);
$token = db_GetOne("SELECT token FROM WEBHOOKS_DETS WHERE identificador = '$id'");
echo $token;
A intenção é simples: buscar uma linha. O problema é que $id é texto livre. trim() remove espaços; não neutraliza SQL.
Com identificador=' OR 1=1 --, a consulta montada vira:
SELECT token FROM WEBHOOKS_DETS WHERE identificador = '' OR 1=1 --'
O OR 1=1 casa com a primeira linha da tabela e o -- comenta o resto. O atacante recebe um token que não era dele.
O que o atacante consegue fazer
Injeção não é só "ver um dado a mais". Dependendo do privilégio do usuário do banco, o impacto escala rápido:
| Payload | Efeito |
|---|---|
' OR 1=1 -- |
retorna a primeira linha da tabela |
x' UNION SELECT senha FROM USUARIOS -- |
lê outra tabela |
x' AND (SELECT COUNT(*) FROM USUARIOS) > 0 -- |
inferência cega por tempo/resposta |
'; UPDATE USUARIOS SET ATIVO = 'N' -- |
escrita indevida, quando o driver permite múltiplos comandos |
No Firebird, o UNION é o caminho mais direto para vazar dados de outras tabelas. E endpoints públicos — webhooks, callbacks, captura de campanha — são a porta preferida porque não passam por sessão.
Por que trim e addslashes não resolvem
trim()remove espaços nas pontas. Aspas,--eUNIONcontinuam intactos.addslashes()depende do conjunto de caracteres e do modo de escape do banco. Em ISO-8859-1 mal configurado, é possível contornar o escape.magic_quotesfoi removido do PHP há anos e nunca foi uma solução.- Validar o formato é bom, mas não é a causa raiz. Basta aparecer outro parâmetro, outro endpoint ou outro formato aceito.
A causa é a concatenação. A correção precisa acontecer no ponto onde o SQL é montado.
A solução: bind de parâmetros
O driver ibase suporta parâmetros posicionais com ?. O SQL e os valores viajam separados; o banco trata o valor como dado, nunca como comando.
Com um wrapper comum em sistemas PHP/Firebird:
$token = db_GetOne(
'SELECT token FROM WEBHOOKS_DETS WHERE identificador = ?',
[$id]
);
Com ibase puro:
$stmt = ibase_prepare($conn, 'SELECT token FROM WEBHOOKS_DETS WHERE identificador = ?');
$res = ibase_execute($stmt, $id);
$token = $res ? ibase_fetch_row($res)[0] : '';
Ou, quando o wrapper aceita parâmetros inline:
$res = ibase_query($conn, 'SELECT token FROM WEBHOOKS_DETS WHERE identificador = ?', $id);
O driver envia a query e o array de valores em chamadas distintas (ibase_query($conn, $sql, ...$params)). Por isso ' OR 1=1 -- deixa de ser sintaxe e passa a ser apenas uma string procurada na coluna, que não existe.
Antes e depois
| Entrada | Antes | Depois |
|---|---|---|
identificador válido |
token correto | token correto |
zzz' OR '1'='1 |
token de outra linha | vazio |
x' |
erro de SQL do Firebird | vazio, sem erro |
| parâmetro ausente | warning no PHP | vazio, resposta 200 |
identificador[]=a |
warning de array | vazio, resposta 200 |
Note a última linha: além da injeção, o bind com validação de tipo evita avisos que revelam detalhes internos.
Defesa em profundidade
Bind resolve a injeção, mas algumas camadas extras custam pouco:
Valide o formato esperado. Um identificador numérico pode ser checado antes de consultar:
if (!preg_match('/^\d{5,25}$/', $id)) { echo ''; exit; }Não exiba erro de SQL para o usuário. Em produção,
DEBUGligado transforma um erro em mapa do banco. Logue internamente e devolva resposta genérica.Aplique o bind na escrita também.
UPDATE,DELETEeINSERTconcatenados têm o mesmo problema, com dano maior.Use o menor privilégio possível. Um endpoint de leitura não precisa de permissão para alterar tabelas, criar objetos ou ler outras tabelas.
Revise os pontos sem login. Webhooks, callbacks e endpoints de campanha são a porta de entrada preferida porque não passam por sessão.
Trate o parâmetro como ausente.
$_GET['x']semissetgera warning e pode virar string vazia silenciosa. Useisset+is_string.Teste com cargas maliciosas. Aspas,
--,UNION,\0, arrays e valores vazios. O teste feliz não prova nada.
Como caçar injeção no código
Procure os padrões que denunciam concatenação:
"SELECT ... '$var'"
"WHERE ... $var"
"... = $var"
"ORDER BY $var"
"LIMIT $var"
Os dois últimos são fáceis de esquecer: ORDER BY e paginação também recebem dados externos e não aceitam bind em todos os casos. Para esses, use uma lista branca de colunas e direções permitidas.
$colunas = ['nome', 'data', 'valor'];
$ordem = in_array($_GET['ordem'] ?? '', $colunas, true) ? $_GET['ordem'] : 'data';
$dir = ($_GET['dir'] ?? 'asc') === 'desc' ? 'DESC' : 'ASC';
$sql = "SELECT * FROM CLIENTES ORDER BY $ordem $dir";
Aqui a variável interpolada é segura porque só pode assumir valores de uma lista fixa.
Passo a passo para corrigir um endpoint
- Reproduza com um payload que comprove o vazamento ou o erro.
- Localize a montagem da string e identifique todos os parâmetros externos.
- Troque por
?com array de parâmetros. - Valide o formato quando ele é conhecido.
- Teste entrada válida, inválida, ausente, array e payloads clássicos.
- Confira os logs para garantir que não sobrou erro de SQL exposto.
- Monitore após o deploy, especialmente se o endpoint for público.
Conclusão
SQL injection no Firebird não é um problema de banco exótico: é o mesmo erro de sempre, concatenar dado externo em comando. O bind elimina a raiz, a validação de formato reduz a superfície e o menor privilégio limita o estrago se algo passar.
O Sqlify gera SQL Firebird a partir de português e usa parâmetros posicionais nos exemplos, para você começar com o pé direito. Descreva a consulta em português e receba a query pronta, com a explicação de cada parte.
Leia também: