Como proteger o site do servidor de MU contra SQL injection
Entenda como o SQL injection ataca o site do seu servidor de MU Online e aplique defesas reais com prepared statements, validação, privilégios mínimos e monitoramento para blindar contas e itens.
O site é a porta de entrada do seu servidor de MU Online — e, por isso mesmo, o alvo preferido de quem quer atalho. Entre todas as falhas web, o SQL injection é a mais perigosa para um servidor de MU porque atinge diretamente o banco que guarda contas, senhas, saldos de créditos e itens. Uma única q
O site é a porta de entrada do seu servidor de MU Online — e, por isso mesmo, o alvo preferido de quem quer atalho. Entre todas as falhas web, o SQL injection é a mais perigosa para um servidor de MU porque atinge diretamente o banco que guarda contas, senhas, saldos de créditos e itens. Uma única query mal escrita num formulário de login pode permitir que um invasor entre sem senha, leia o banco inteiro, se dê itens e WCoin, promova a própria conta a GM ou simplesmente apague tudo. Não é exagero: a maioria dos "hacks" de servidores privados que viram história de fórum começou num campo de texto concatenado direto no SQL.
Este é um guia avançado, orientado à prática, para blindar o site contra esse ataque. Vamos ver como o injection realmente funciona, aplicar as defesas na ordem certa de importância — prepared statements, validação, privilégio mínimo, tratamento de erro e monitoramento — e revisar os pontos do site de MU que mais são atacados. Se você ainda está estruturando a base do projeto, vale começar por como criar um servidor de MU Online e voltar para endurecer a camada web.
Pré-requisitos
Para acompanhar e aplicar as correções você precisa de:
- Acesso ao código-fonte do site (PHP na maioria dos casos, mas o conceito vale para qualquer linguagem).
- Acesso administrativo ao banco (SQL Server ou MySQL/MariaDB) para ajustar usuários e privilégios.
- Um ambiente de testes separado da produção para validar as mudanças sem risco.
- Conhecimento básico de SQL e de como o site conecta ao banco (a connection string).
- Backup completo e recente do banco antes de qualquer alteração.
> Aviso de segurança: nunca teste payloads de injection no banco de produção. Trabalhe sempre em uma cópia de teste. E nunca exponha, em código público ou prints, a sua connection string real.
Como o SQL injection funciona (o problema na raiz)
O ataque nasce de um erro conceitual simples: misturar comando (SQL) com dados (o que o usuário digitou). Veja um login vulnerável, do tipo que ainda circula em muitos sites de MU:
<?php
// CÓDIGO VULNERÁVEL — nunca use isto
$user = $_POST['user'];
$pass = $_POST['pass'];
$sql = "SELECT * FROM MEMB_INFO WHERE memb___id = '$user' AND memb__pwd = '$pass'";
$result = sqlsrv_query($conn, $sql);
O que o programador espera é que o usuário digite algo como guerreiro123. Mas o campo aceita qualquer texto. Se o invasor digitar no campo de usuário:
' OR '1'='1' --
A query montada vira:
SELECT * FROM MEMB_INFO WHERE memb___id = '' OR '1'='1' --' AND memb__pwd = '...'
O OR '1'='1' é sempre verdadeiro, e o -- comenta o resto da query (incluindo a checagem de senha). Resultado: o login retorna o primeiro usuário da tabela, e o invasor entra sem saber senha nenhuma. Variações mais graves usam UNION SELECT para ler outras tabelas, ou ; UPDATE / ; DROP para alterar e destruir dados.
O ponto central: o banco não tem como saber que ' OR '1'='1' era pra ser um nome de usuário e não parte do comando, porque tudo chegou misturado numa string só. A defesa raiz é separar as duas coisas.
Defesa 1 (a mais importante) — Prepared statements
Prepared statements (consultas parametrizadas) resolvem o problema na origem: você envia o comando com marcadores (? ou :nome) e, separadamente, os dados. O banco monta a query já sabendo o que é comando e o que é valor — o texto do usuário nunca é interpretado como SQL.
Veja o mesmo login, agora seguro, com PDO:
<?php
// CÓDIGO SEGURO — prepared statement com PDO
$stmt = $pdo->prepare(
"SELECT memb_guid, memb___id FROM MEMB_INFO
WHERE memb___id = :user AND memb__pwd = :pass"
);
$stmt->execute([
':user' => $_POST['user'],
':pass' => hash_senha($_POST['pass']), // senha nunca em texto puro
]);
$conta = $stmt->fetch(PDO::FETCH_ASSOC);
if ($conta) {
// login válido
} else {
// credenciais inválidas
}
Agora, se o invasor digitar ' OR '1'='1' --, esse texto vira literalmente o valor procurado no campo memb___id. Como não existe usuário com esse nome bizarro, o login falha — que é exatamente o comportamento correto. O mesmo vale para SQL Server via sqlsrv com parâmetros:
<?php
$sql = "SELECT memb_guid FROM MEMB_INFO WHERE memb___id = ?";
$params = [ $_POST['user'] ];
$stmt = sqlsrv_query($conn, $sql, $params); // parâmetros separados do comando
Regra de ouro: toda query que use qualquer valor vindo do usuário — $_POST, $_GET, $_COOKIE, cabeçalhos, dados de API — precisa ser parametrizada. Não existe exceção "esse campo é só número, tá seguro". Números também são injetáveis.
O caso especial: partes que não aceitam bind
Prepared statements parametrizam valores, mas não nomes de tabela, nomes de coluna nem a direção de um ORDER BY. Isso é comum em rankings de MU que ordenam por coluna escolhida pelo usuário:
<?php
// Perigoso: coluna e direção vindo do usuário
$col = $_GET['sort']; // ex.: "resets" — mas pode ser injection
$sql = "SELECT Name, Resets FROM Character ORDER BY $col DESC"; // VULNERÁVEL
A defesa aqui é uma allowlist (lista fechada de valores permitidos), nunca o valor cru:
<?php
$permitidas = ['Name' => 'Name', 'resets' => 'Resets', 'level' => 'cLevel'];
$col = $permitidas[$_GET['sort'] ?? 'resets'] ?? 'Resets'; // só valores conhecidos
$dir = ($_GET['dir'] ?? 'desc') === 'asc' ? 'ASC' : 'DESC';
$sql = "SELECT Name, Resets FROM Character ORDER BY $col $dir";
Assim, o usuário só consegue escolher entre colunas que você explicitamente autorizou; qualquer outra coisa cai no valor padrão.
Defesa 2 — Validação e sanitização de entrada
Prepared statements impedem o injection, mas validar a entrada é uma camada extra que corta ataques antes mesmo de chegarem ao banco e melhora a qualidade dos dados. A ideia é: aceite apenas o que faz sentido para cada campo.
<?php
// Nome de usuário do MU: normalmente 4 a 10 caracteres, letras e números
function validarLogin(string $u): bool {
return (bool) preg_match('/^[A-Za-z0-9]{4,10}$/', $u);
}
if (!validarLogin($_POST['user'])) {
// rejeita antes de tocar no banco
exit('Usuário inválido');
}
Valide o tipo e o formato de tudo: IDs devem ser inteiros ((int)$_GET['id'] ou filter_var(..., FILTER_VALIDATE_INT)), e-mails com FILTER_VALIDATE_EMAIL, nomes de personagem contra o padrão da sua Season. Validação é sobre aceitar o esperado (allowlist), não sobre tentar bloquear o proibido (blocklist) — filtrar "palavras perigosas" é frágil e sempre tem como contornar.
| Campo típico do site MU | Regra recomendada |
|---|---|
| Login / usuário | Allowlist regex, tamanho fixo (ex.: 4-10 alfanumérico) |
| Senha | Tamanho mínimo, hash antes de comparar, nunca em texto puro |
| ID numérico (personagem, pedido) | Inteiro estrito com FILTER_VALIDATE_INT |
| E-mail (recuperação) | FILTER_VALIDATE_EMAIL |
| Coluna de ordenação (ranking) | Allowlist fechada de colunas |
| Nome de personagem | Padrão da Season (comprimento e caracteres) |
Defesa 3 — Privilégio mínimo no usuário do banco
Mesmo que uma falha passe, você quer limitar o estrago. O site nunca deve conectar ao banco com um usuário sa (SQL Server) ou root (MySQL). Crie um usuário dedicado com apenas os privilégios necessários.
-- MySQL: usuário só com o que o site precisa
CREATE USER 'web_mu'@'localhost' IDENTIFIED BY 'senha-forte-aqui';
GRANT SELECT, INSERT, UPDATE ON muonline.* TO 'web_mu'@'localhost';
-- Repare: sem DROP, sem DELETE amplo, sem ALTER, sem GRANT
FLUSH PRIVILEGES;
Com esse usuário, um DROP TABLE injetado simplesmente falha por falta de permissão. Se determinado formulário só lê dados (um ranking, por exemplo), o ideal é que ele use uma conexão apenas com SELECT. Separe, quando possível, conexões de leitura das de escrita. Isso transforma uma catástrofe em um incidente contido.
Defesa 4 — Tratamento de erros sem vazar informação
Sites vulneráveis costumam entregar o mapa do banco de graça: quando uma query falha, exibem a mensagem de erro completa do SQL na tela, revelando nomes de tabela, colunas e a estrutura. Isso é ouro para o atacante (a técnica se chama error-based injection).
Nunca mostre erro de banco ao usuário em produção:
<?php
// Em produção: registre o erro internamente, mostre mensagem genérica
try {
$stmt->execute($params);
} catch (PDOException $e) {
error_log('DB error: ' . $e->getMessage()); // vai para o log, não para a tela
http_response_code(500);
exit('Ocorreu um erro. Tente novamente mais tarde.'); // genérico
}
No PHP, garanta em produção display_errors = Off no php.ini, mantendo log_errors = On. Assim você continua vendo os erros nos logs para depurar, mas o visitante nunca vê a estrutura interna.
Defesa 5 — Camadas extras: WAF e monitoramento
Depois de corrigir o código, adicione camadas de vigilância. Um WAF (Web Application Firewall) — como o ModSecurity com o conjunto de regras OWASP CRS — barra padrões de ataque conhecidos antes de chegarem à aplicação. Ele não substitui a correção no código, mas dá uma rede de segurança e tempo de reação.
Monte também um monitoramento simples para detectar tentativas:
<?php
// Detecção leve de payloads suspeitos para alerta (NÃO como defesa principal)
function pareceInjection(string $v): bool {
return (bool) preg_match(
'/(\bUNION\b|\bSELECT\b.+\bFROM\b|--|\bOR\b\s+\d+=\d+|;\s*DROP)/i', $v
);
}
foreach ($_REQUEST as $k => $v) {
if (is_string($v) && pareceInjection($v)) {
error_log("Tentativa suspeita em '$k' do IP {$_SERVER['REMOTE_ADDR']}: $v");
// dispare alerta (e-mail/Discord) e considere bloquear o IP
}
}
Ative os logs de query do banco em pontos sensíveis e configure alertas para picos de erro. Detectar cedo é a diferença entre bloquear um IP e descobrir, semanas depois, que meio servidor recebeu itens ilegais.
Pontos do site de MU que mais são atacados
Priorize a revisão nestes fluxos, do mais crítico ao menos:
- Login — o alvo número um; bypass dá acesso a contas.
- Cadastro — injeção pode criar contas privilegiadas ou corromper dados.
- Recuperação de senha — costuma montar queries por e-mail/pergunta secreta.
- Ranking e listagens — o
ORDER BYdinâmico é um clássico esquecido. - Loja / doação — mexe com saldo e itens; injection aqui vira fraude direta.
- Painel do jogador — troca de senha, e-mail, transferências de personagem.
Revise cada um garantindo prepared statements, validação e o usuário de banco com privilégio mínimo.
Erros comuns e soluções
| Erro / má prática | Risco | Correção |
|---|---|---|
Concatenar $_POST direto na query | Bypass de login, roubo de banco | Migrar para prepared statement |
| Confiar que "campo numérico é seguro" | Injection por parâmetro numérico | Validar inteiro e ainda assim parametrizar |
ORDER BY $coluna do usuário | Injection na cláusula ORDER BY | Allowlist fechada de colunas |
Site conecta como sa/root | Um DROP injetado apaga tudo | Usuário dedicado com privilégio mínimo |
| Exibir erro de SQL na tela | Vaza estrutura (error-based) | display_errors Off, mensagem genérica |
| Filtrar só "palavras perigosas" | Contornável, falsa sensação de segurança | Allowlist de formato, não blocklist |
| Senha comparada em texto puro | Roubo direto de credenciais | Hash forte e comparação por hash |
| Sem logs nem alertas | Ataque descoberto tarde demais | Monitorar payloads e picos de erro |
Checklist de lançamento
- Backup completo do banco feito antes de qualquer alteração
- Todas as queries com dados do usuário migradas para prepared statements
- Cláusulas ORDER BY / nomes de coluna dinâmicos com allowlist
- Validação de tipo e formato em todos os campos de entrada
- Usuário do banco dedicado, com privilégio mínimo (sem DROP/ALTER/GRANT)
- Conexões de leitura separadas das de escrita quando possível
- display_errors desligado em produção, log_errors ligado
- Mensagens de erro genéricas para o usuário, detalhes só no log
- Senhas sempre com hash, nunca comparadas em texto puro
- WAF (ex.: ModSecurity + OWASP CRS) avaliado como camada extra
- Monitoramento de payloads suspeitos e alertas configurados
- Fluxos críticos revisados: login, cadastro, recuperação, ranking, loja
- Tudo testado em ambiente separado antes de subir para produção
SQL injection é uma ameaça séria, mas totalmente evitável. A base é sempre a mesma: separe comando de dados com prepared statements, valide o que entra, dê ao site o mínimo de poder no banco e vigie o que acontece. Aplicando essas camadas, você tira do atacante o atalho mais fácil e protege de verdade as contas e os itens dos seus jogadores.
Perguntas frequentes
SQL injection é realmente comum em site de MU Online?
É uma das falhas mais exploradas em servidores privados. Muitos sites usam scripts antigos que concatenam login e senha direto na query. Um invasor consegue burlar o login, roubar contas, dar itens ou até apagar o banco por um formulário vulnerável.
Prepared statements sozinhos resolvem tudo?
Resolvem a maior parte, pois separam comando de dados. Mas há casos que exigem cuidado extra: nomes de tabela/coluna dinâmicos e cláusulas ORDER BY não aceitam bind e precisam de allowlist. Combine prepared statements com validação de entrada e privilégio mínimo.
Preciso trocar todo o meu site antigo para me proteger?
Nem sempre, mas todo ponto que toca o banco precisa ser revisado. Formulários de login, cadastro, ranking, recuperação de senha e loja são os alvos principais. Migre cada query para prepared statement e valide as entradas.
Um WAF substitui a correção do código?
Não. Um WAF (firewall de aplicação) ajuda a barrar ataques conhecidos e ganha tempo, mas é uma camada extra, não a solução. A correção real é no código, com prepared statements e privilégio mínimo no usuário do banco.
Como sei se meu site já foi atacado por SQL injection?
Procure por padrões suspeitos nos logs (aspas, UNION SELECT, OR 1=1, comentários -- em parâmetros), contas criadas ou alteradas sem origem, itens que apareceram do nada e picos de erro no banco. Ative logs de query e alertas para reagir cedo.