Como criar um sistema de registro de conta seguro para MU Online
Construa um sistema de registro de conta em PHP para o seu servidor de MU Online blindado contra SQL injection, senhas fracas e bots, usando prepared statements, hashing correto e validação rigorosa.
O formulário de registro é a porta de entrada do seu servidor de MU Online e, ao mesmo tempo, um dos alvos preferidos de quem quer atacá-lo. Um registro mal feito abre caminho para SQL injection (que pode destruir ou vazar todo o banco), permite senhas ridiculamente fracas, deixa bots criarem milhar
O formulário de registro é a porta de entrada do seu servidor de MU Online e, ao mesmo tempo, um dos alvos preferidos de quem quer atacá-lo. Um registro mal feito abre caminho para SQL injection (que pode destruir ou vazar todo o banco), permite senhas ridiculamente fracas, deixa bots criarem milhares de contas fantasmas e, no pior caso, expõe as credenciais dos jogadores. Este tutorial, de nível avançado, mostra como construir um sistema de registro em PHP que resiste a esses ataques usando os pilares corretos: prepared statements, hashing de senha, validação rigorosa no servidor e defesa contra automação. O foco é conceito e segurança real, não um copiar-e-colar cego.
Antes de tudo, entenda o modelo de ameaça. O registro recebe dados que vêm de fora, ou seja, de gente que pode ser mal-intencionada. A regra número um da segurança é: toda entrada é hostil até prova em contrário. Cada campo do formulário precisa ser tratado como potencial ataque. A partir dessa mentalidade, cada decisão técnica deste tutorial faz sentido.
Pré-requisitos
- Servidor de MU e banco funcionando. Se ainda está na base, comece pelo guia de como criar servidor de MU Online.
- PHP 7.4+ ou 8.x com PDO e o driver do seu SGBD (normalmente SQL Server via
sqlsrv/PDO). - HTTPS configurado no domínio (Let's Encrypt resolve de graça).
- Conhecimento do esquema de conta do seu emulador: nome da tabela e das colunas de usuário/senha/e-mail.
- Um usuário de banco de baixo privilégio para o site.
| Ameaça | Vetor | Defesa neste tutorial |
|---|---|---|
| SQL injection | Campos do formulário concatenados na query | Prepared statements sempre |
| Roubo de senha | Senha em texto puro no banco ou no tráfego | Hashing + HTTPS |
| Bots/spam | Registro automatizado em massa | CAPTCHA, rate limit, honeypot, e-mail |
| Força bruta | Adivinhação de senha | Rate limiting e senhas fortes obrigatórias |
| Enumeração de contas | Mensagens que revelam se um usuário existe | Respostas genéricas |
> Aviso: nomes de tabela e coluna (MEMB_INFO, memb___id, memb__pwd, mail_addr) são exemplos da linha Season 6. O formato real, inclusive o algoritmo de senha esperado, varia por emulador. Confirme antes de implementar.
O dilema do hash de senha no MU
Aqui está a tensão central de qualquer registro de MU. O ideal absoluto de segurança é gravar senhas com um algoritmo forte e lento como bcrypt/Argon2 (password_hash() no PHP). O problema: o emulador do jogo precisa validar essa mesma senha no login, e muitos emuladores antigos só entendem MD5 ou até texto puro. Você não controla o algoritmo isoladamente, ele precisa bater com o que o servidor de jogo espera.
Portanto, duas situações:
- Emulador moderno que suporta hash forte — use
password_hash()e viva feliz. - Emulador legado preso a MD5 — você fica limitado ao formato que o jogo aceita. Nesse caso, mitigue o risco com HTTPS obrigatório, rate limiting, monitoramento e jamais reutilizar esse banco para outra finalidade.
Sempre confirme o formato esperado, pois varia por versão. Abaixo mostro os dois caminhos.
Passo 1: Conectar ao banco com PDO
Nunca use a API antiga sem prepared statements. Configure PDO forçando exceções e desativando emulação de prepared:
<?php
// db.php
function getConnection(): PDO {
$dsn = 'sqlsrv:Server=203.0.113.10,1433;Database=MuOnline';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // prepared reais, não emulados
];
return new PDO($dsn, 'web_mu', getenv('DB_PASS'), $options);
}
Repare que a senha do banco vem de variável de ambiente (getenv), nunca hardcoded no arquivo versionado.
Passo 2: Validar toda entrada no servidor
Validação em JavaScript é conveniência, não segurança, pois é trivial de burlar. Toda regra precisa ser reforçada em PHP:
<?php
function validarRegistro(array $in): array {
$erros = [];
// Usuário: 4-10 caracteres alfanuméricos (limite comum do MU)
if (!preg_match('/^[a-zA-Z0-9]{4,10}$/', $in['usuario'] ?? '')) {
$erros[] = 'Usuário deve ter 4 a 10 caracteres alfanuméricos.';
}
// E-mail válido
if (!filter_var($in['email'] ?? '', FILTER_VALIDATE_EMAIL)) {
$erros[] = 'E-mail inválido.';
}
// Senha forte: mínimo 8, com letra e número
$senha = $in['senha'] ?? '';
if (strlen($senha) < 8 || !preg_match('/[A-Za-z]/', $senha) || !preg_match('/[0-9]/', $senha)) {
$erros[] = 'Senha deve ter ao menos 8 caracteres, com letras e números.';
}
// Confirmação de senha
if ($senha !== ($in['senha2'] ?? '')) {
$erros[] = 'As senhas não conferem.';
}
return $erros;
}
O limite de tamanho do usuário existe também porque o campo no banco do MU costuma ser curto (varchar(10)), e passar disso trunca ou quebra o login no jogo.
Passo 3: Verificar unicidade com prepared statement
Antes de inserir, cheque se o usuário já existe, sempre com placeholder, nunca concatenando:
<?php
function usuarioExiste(PDO $pdo, string $usuario): bool {
// NUNCA: "SELECT ... WHERE memb___id = '$usuario'" <- injeção!
$stmt = $pdo->prepare('SELECT 1 FROM MEMB_INFO WHERE memb___id = ? ');
$stmt->execute([$usuario]); // dado tratado como dado, não como SQL
return (bool) $stmt->fetchColumn();
}
A diferença entre a linha comentada e a real é a diferença entre um banco seguro e um banco destruído por um '; DROP TABLE MEMB_INFO;--.
Passo 4: Inserir a conta com hash apropriado
Aqui aplicamos a decisão do hash. Caminho ideal, para emulador moderno:
<?php
$hash = password_hash($senha, PASSWORD_DEFAULT); // bcrypt/Argon2, com salt automático
$stmt = $pdo->prepare(
'INSERT INTO MEMB_INFO (memb___id, memb__pwd, mail_addr, bloc_code, ctl1_code)
VALUES (?, ?, ?, 0, 0)'
);
$stmt->execute([$usuario, $hash, $email]);
Caminho legado, para emulador preso a MD5 (menos seguro, use apenas se obrigatório):
<?php
// Só porque o emulador exige. Mitigue com HTTPS + rate limit + monitoramento.
$hashLegado = strtoupper(md5($senha)); // formato varia por emulador
$stmt = $pdo->prepare(
'INSERT INTO MEMB_INFO (memb___id, memb__pwd, mail_addr) VALUES (?, ?, ?)'
);
$stmt->execute([$usuario, $hashLegado, $email]);
Em ambos os casos, a senha nunca vai para o banco em texto puro e a query usa placeholders.
Passo 5: Defesa contra bots e automação
Um formulário aberto atrai registro em massa. Combine camadas, porque nenhuma sozinha basta:
- CAPTCHA (reCAPTCHA/hCaptcha/turnstile) valida que há um humano.
- Honeypot: um campo invisível que humanos não preenchem, mas bots sim. Se vier preenchido, rejeite.
- Rate limiting por IP: no máximo N registros por IP por hora.
- Verificação por e-mail: a conta só ativa após clicar no link enviado.
Honeypot é barato e eficaz:
<?php
// Campo escondido por CSS no formulário: <input name="website" style="display:none">
if (!empty($_POST['website'])) {
// Humano não vê esse campo; se veio preenchido, é bot.
http_response_code(400);
exit('Requisição inválida.');
}
Rate limiting simples registrando tentativas:
<?php
function dentroDoLimite(PDO $pdo, string $ip, int $max = 5): bool {
$stmt = $pdo->prepare(
'SELECT COUNT(*) FROM registro_log
WHERE ip = ? AND criado_em > DATEADD(hour, -1, GETDATE())'
);
$stmt->execute([$ip]);
return (int) $stmt->fetchColumn() < $max;
}
Passo 6: Proteger contra CSRF
Para impedir que outro site force um envio do seu formulário, gere um token por sessão e valide no POST:
<?php
session_start();
// Ao exibir o formulário:
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
// No HTML: <input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">
// Ao processar o POST:
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
http_response_code(403);
exit('Token inválido.');
}
Passo 7: Respostas genéricas e HTTPS
Evite mensagens que revelem se um usuário ou e-mail já existe, pois isso permite enumeração de contas. Prefira "Se os dados estiverem corretos, você receberá um e-mail". E force HTTPS sempre, redirecionando qualquer acesso HTTP, para que senha e dados nunca trafeguem em texto puro.
<?php
// Forçar HTTPS
if (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === 'off') {
header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], true, 301);
exit;
}
Passo 8: Registrar tudo para auditoria
Logue tentativas de registro (IP, horário, resultado) sem gravar a senha. Esses logs permitem detectar ataques, bloquear IPs abusivos e investigar incidentes. Nunca registre a senha em log, nem com hash.
Erros comuns e soluções
| Erro | Consequência | Solução |
|---|---|---|
| Concatenar entrada na query | SQL injection, banco comprometido | Usar prepared statements em todas as queries |
| Gravar senha em texto puro | Vazamento total em caso de invasão | Hash com password_hash (ou o formato do emulador) |
| Validar só em JavaScript | Bypass trivial, dados sujos no banco | Reforçar toda validação no servidor |
| Sem HTTPS | Senha capturada em trânsito | Certificado Let's Encrypt e redirecionamento forçado |
| Formulário sem anti-bot | Milhares de contas fantasmas | CAPTCHA + honeypot + rate limit + e-mail |
| Mensagens específicas de erro | Enumeração de contas | Respostas genéricas |
| Senha do banco no código versionado | Credencial exposta em repositório | Usar variável de ambiente |
| Sem token CSRF | Registro forçado por terceiros | Token por sessão validado com hash_equals |
Checklist de lançamento
- Conexão PDO com
ERRMODE_EXCEPTIONeEMULATE_PREPARES=false - Todas as queries usam prepared statements, sem exceção
- Senha nunca gravada em texto puro
- Formato de hash confirmado com o emulador (varia por versão)
- Validação completa reforçada no servidor
- Limite de tamanho do usuário compatível com a coluna do banco
- HTTPS obrigatório com redirecionamento de HTTP
- CAPTCHA ativo no formulário
- Honeypot invisível implementado
- Rate limiting por IP funcionando
- Verificação por e-mail antes de ativar a conta
- Token CSRF gerado e validado
- Mensagens genéricas para evitar enumeração
- Credenciais do banco em variáveis de ambiente
- Logs de tentativa sem gravar senha
- Usuário de banco de baixo privilégio
Um sistema de registro seguro não é um único truque, e sim a soma de várias camadas: prepared statements que fecham a porta da injeção, hashing que protege as senhas mesmo em caso de vazamento, validação de servidor que rejeita lixo, e defesas anti-bot que mantêm o banco limpo. Nenhuma camada é opcional. O jogador confia a você a senha dele, muitas vezes a mesma que usa em outros lugares, e essa confiança é o ativo mais valioso do seu servidor. Construa o registro pensando como um atacante pensaria, e você terá uma fundação sólida sobre a qual todo o resto do projeto pode crescer com tranquilidade.
Perguntas frequentes
Posso usar MD5 nas senhas porque o emulador usa?
Muitos emuladores antigos armazenam senha em MD5 ou até texto puro por compatibilidade. Se o seu exige isso, você fica preso a esse formato para o login funcionar no jogo. O ideal é usar emulador que suporte hash forte; se não der, mitigue com HTTPS, rate limiting e monitoramento, e nunca reutilize esse banco para nada além do jogo.
Prepared statements realmente impedem SQL injection?
Sim, quando usados corretamente. Eles separam o comando SQL dos dados, então a entrada do usuário nunca é interpretada como código. O erro está em concatenar entrada na query mesmo tendo prepared statements disponíveis, ou em usar prepared só em parte das queries. Use em todas, sem exceção.
Como impedir que bots criem milhares de contas?
Combine várias camadas: CAPTCHA no formulário, rate limiting por IP, verificação por e-mail e honeypot invisível. Nenhuma sozinha resolve, mas juntas elevam muito o custo do ataque. Registre também tentativas para detectar padrões de abuso e bloquear IPs reincidentes.
Preciso de HTTPS mesmo em servidor pequeno?
Sim, sempre. Sem HTTPS, senha e dados trafegam em texto puro e qualquer um na mesma rede captura tudo. Certificados gratuitos via Let's Encrypt tornam isso trivial e sem custo. Não existe justificativa para registro de conta sem HTTPS em 2024.
Onde os dados do registro devem ser validados?
Sempre no servidor, em PHP. Validação em JavaScript melhora a experiência do usuário, mas é trivial de burlar, então serve só de conveniência. Toda regra que protege o banco (formato, tamanho, unicidade, caracteres permitidos) precisa ser reforçada no backend antes de tocar o SQL.