O maior portal de MU Online do Brasil — desde 2003
Tutorial Avançado Website

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.

GA Gabriel · Atualizado em 5 out 2024 · ⏱ 19 min de leitura
Resposta rápida

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çaVetorDefesa neste tutorial
SQL injectionCampos do formulário concatenados na queryPrepared statements sempre
Roubo de senhaSenha em texto puro no banco ou no tráfegoHashing + HTTPS
Bots/spamRegistro automatizado em massaCAPTCHA, rate limit, honeypot, e-mail
Força brutaAdivinhação de senhaRate limiting e senhas fortes obrigatórias
Enumeração de contasMensagens que revelam se um usuário existeRespostas 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:

  1. Emulador moderno que suporta hash forte — use password_hash() e viva feliz.
  2. 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

ErroConsequênciaSolução
Concatenar entrada na querySQL injection, banco comprometidoUsar prepared statements em todas as queries
Gravar senha em texto puroVazamento total em caso de invasãoHash com password_hash (ou o formato do emulador)
Validar só em JavaScriptBypass trivial, dados sujos no bancoReforçar toda validação no servidor
Sem HTTPSSenha capturada em trânsitoCertificado Let's Encrypt e redirecionamento forçado
Formulário sem anti-botMilhares de contas fantasmasCAPTCHA + honeypot + rate limit + e-mail
Mensagens específicas de erroEnumeração de contasRespostas genéricas
Senha do banco no código versionadoCredencial exposta em repositórioUsar variável de ambiente
Sem token CSRFRegistro forçado por terceirosToken por sessão validado com hash_equals

Checklist de lançamento

  • Conexão PDO com ERRMODE_EXCEPTION e EMULATE_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.

GA
Editor de guias e builds

Gabriel cobre gameplay, builds de classes, PvP e progressão. Testa cada estratégia em servidor antes de publicar.

Continue lendo

Artigos relacionados