El mayor portal de MU Online de Brasil — desde 2003
Tutorial Avanzado Website

Cómo proteger el sitio del servidor de MU contra SQL injection

Entiende cómo el SQL injection ataca el sitio de tu servidor de MU Online y aplica defensas reales con prepared statements, validación, privilegios mínimos y monitoreo para blindar cuentas e ítems.

GA Gabriel · Actualizado el 10 jul 2026 · ⏱ 16 min de lectura
Respuesta rápida

El sitio es la puerta de entrada de tu servidor de MU Online — y, precisamente por eso, el blanco preferido de quien busca un atajo. Entre todas las fallas web, el SQL injection es la más peligrosa para un servidor de MU porque golpea directamente la base de datos que guarda cuentas, contraseñas, sa

El sitio es la puerta de entrada de tu servidor de MU Online — y, precisamente por eso, el blanco preferido de quien busca un atajo. Entre todas las fallas web, el SQL injection es la más peligrosa para un servidor de MU porque golpea directamente la base de datos que guarda cuentas, contraseñas, saldos de créditos e ítems. Una sola query mal escrita en un formulario de login puede permitir que un intruso entre sin contraseña, lea la base entera, se dé ítems y WCoin, promueva su propia cuenta a GM o simplemente lo borre todo. No es exageración: la mayoría de los "hacks" de servidores privados que se volvieron historia de foro empezaron en un campo de texto concatenado directo en el SQL.

Esta es una guía avanzada, orientada a la práctica, para blindar el sitio contra ese ataque. Vamos a ver cómo funciona realmente el injection, aplicar las defensas en el orden correcto de importancia — prepared statements, validación, privilegio mínimo, tratamiento de error y monitoreo — y revisar los puntos del sitio de MU que más son atacados. Si todavía estás estructurando la base del proyecto, vale la pena empezar por cómo crear un servidor de MU Online y volver para endurecer la capa web.

Requisitos previos

Para seguir y aplicar las correcciones necesitas:

  • Acceso al código fuente del sitio (PHP en la mayoría de los casos, pero el concepto vale para cualquier lenguaje).
  • Acceso administrativo a la base de datos (SQL Server o MySQL/MariaDB) para ajustar usuarios y privilegios.
  • Un entorno de pruebas separado de la producción para validar los cambios sin riesgo.
  • Conocimiento básico de SQL y de cómo el sitio conecta a la base de datos (la connection string).
  • Respaldo completo y reciente de la base de datos antes de cualquier alteración.

> Aviso de seguridad: nunca pruebes payloads de injection en la base de producción. Trabaja siempre en una copia de prueba. Y nunca expongas, en código público ni capturas, tu connection string real.

Cómo funciona el SQL injection (el problema en la raíz)

El ataque nace de un error conceptual simple: mezclar comando (SQL) con datos (lo que el usuario escribió). Mira un login vulnerable, del tipo que aún circula en muchos sitios 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);

Lo que el programador espera es que el usuario escriba algo como guerrero123. Pero el campo acepta cualquier texto. Si el intruso escribe en el campo de usuario:

' OR '1'='1' --

La query montada queda:

SELECT * FROM MEMB_INFO WHERE memb___id = '' OR '1'='1' --' AND memb__pwd = '...'

El OR '1'='1' es siempre verdadero, y el -- comenta el resto de la query (incluyendo la verificación de contraseña). Resultado: el login devuelve el primer usuario de la tabla, y el intruso entra sin saber ninguna contraseña. Variaciones más graves usan UNION SELECT para leer otras tablas, o ; UPDATE / ; DROP para alterar y destruir datos.

El punto central: la base de datos no tiene forma de saber que ' OR '1'='1' debía ser un nombre de usuario y no parte del comando, porque todo llegó mezclado en una sola string. La defensa raíz es separar las dos cosas.

Defensa 1 (la más importante) — Prepared statements

Los prepared statements (consultas parametrizadas) resuelven el problema en el origen: envías el comando con marcadores (? o :nombre) y, por separado, los datos. La base monta la query ya sabiendo qué es comando y qué es valor — el texto del usuario nunca se interpreta como SQL.

Mira el mismo login, ahora seguro, con 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']), // contraseña nunca en texto plano
]);
$conta = $stmt->fetch(PDO::FETCH_ASSOC);

if ($conta) {
    // login válido
} else {
    // credenciales inválidas
}

Ahora, si el intruso escribe ' OR '1'='1' --, ese texto se vuelve literalmente el valor buscado en el campo memb___id. Como no existe usuario con ese nombre extraño, el login falla — que es exactamente el comportamiento correcto. Lo mismo vale para SQL Server vía sqlsrv con 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 del comando

Regla de oro: toda query que use cualquier valor proveniente del usuario — $_POST, $_GET, $_COOKIE, encabezados, datos de API — necesita ser parametrizada. No existe la excepción "ese campo es solo número, está seguro". Los números también son inyectables.

El caso especial: partes que no aceptan bind

Los prepared statements parametrizan valores, pero no nombres de tabla, nombres de columna ni la dirección de un ORDER BY. Esto es común en rankings de MU que ordenan por columna elegida por el usuario:

<?php
// Peligroso: columna y dirección viniendo del usuario
$col = $_GET['sort']; // ej.: "resets" — pero puede ser injection
$sql = "SELECT Name, Resets FROM Character ORDER BY $col DESC"; // VULNERÁVEL

La defensa aquí es una allowlist (lista cerrada de valores permitidos), nunca el valor crudo:

<?php
$permitidas = ['Name' => 'Name', 'resets' => 'Resets', 'level' => 'cLevel'];
$col = $permitidas[$_GET['sort'] ?? 'resets'] ?? 'Resets'; // solo valores conocidos
$dir = ($_GET['dir'] ?? 'desc') === 'asc' ? 'ASC' : 'DESC';
$sql = "SELECT Name, Resets FROM Character ORDER BY $col $dir";

Así, el usuario solo consigue elegir entre columnas que autorizaste explícitamente; cualquier otra cosa cae en el valor por defecto.

Defensa 2 — Validación y sanitización de entrada

Los prepared statements impiden el injection, pero validar la entrada es una capa extra que corta ataques antes incluso de llegar a la base y mejora la calidad de los datos. La idea es: acepta solo lo que tiene sentido para cada campo.

<?php
// Nombre de usuario de MU: normalmente 4 a 10 caracteres, letras y números
function validarLogin(string $u): bool {
    return (bool) preg_match('/^[A-Za-z0-9]{4,10}$/', $u);
}

if (!validarLogin($_POST['user'])) {
    // rechaza antes de tocar la base de datos
    exit('Usuario inválido');
}

Valida el tipo y el formato de todo: los IDs deben ser enteros ((int)$_GET['id'] o filter_var(..., FILTER_VALIDATE_INT)), los correos con FILTER_VALIDATE_EMAIL, los nombres de personaje contra el patrón de tu Season. La validación consiste en aceptar lo esperado (allowlist), no en intentar bloquear lo prohibido (blocklist) — filtrar "palabras peligrosas" es frágil y siempre hay forma de sortearlo.

Campo típico del sitio MURegla recomendada
Login / usuarioAllowlist regex, tamaño fijo (ej.: 4-10 alfanumérico)
ContraseñaTamaño mínimo, hash antes de comparar, nunca en texto plano
ID numérico (personaje, pedido)Entero estricto con FILTER_VALIDATE_INT
Correo (recuperación)FILTER_VALIDATE_EMAIL
Columna de ordenación (ranking)Allowlist cerrada de columnas
Nombre de personajePatrón de la Season (longitud y caracteres)

Defensa 3 — Privilegio mínimo en el usuario de la base de datos

Aunque una falla pase, quieres limitar el daño. El sitio nunca debe conectar a la base con un usuario sa (SQL Server) o root (MySQL). Crea un usuario dedicado con solo los privilegios necesarios.

-- MySQL: usuario solo con lo que el sitio necesita
CREATE USER 'web_mu'@'localhost' IDENTIFIED BY 'senha-forte-aqui';
GRANT SELECT, INSERT, UPDATE ON muonline.* TO 'web_mu'@'localhost';
-- Nota: sin DROP, sin DELETE amplio, sin ALTER, sin GRANT
FLUSH PRIVILEGES;

Con ese usuario, un DROP TABLE inyectado simplemente falla por falta de permiso. Si determinado formulario solo lee datos (un ranking, por ejemplo), lo ideal es que use una conexión solo con SELECT. Separa, cuando sea posible, las conexiones de lectura de las de escritura. Esto transforma una catástrofe en un incidente contenido.

Defensa 4 — Tratamiento de errores sin filtrar información

Los sitios vulnerables suelen entregar el mapa de la base de datos gratis: cuando una query falla, muestran el mensaje de error completo del SQL en pantalla, revelando nombres de tabla, columnas y la estructura. Eso es oro para el atacante (la técnica se llama error-based injection).

Nunca muestres un error de base de datos al usuario en producción:

<?php
// En producción: registra el error internamente, muestra un mensaje genérico
try {
    $stmt->execute($params);
} catch (PDOException $e) {
    error_log('DB error: ' . $e->getMessage()); // va al log, no a la pantalla
    http_response_code(500);
    exit('Ocorreu um erro. Tente novamente mais tarde.'); // genérico
}

En PHP, garantiza en producción display_errors = Off en el php.ini, manteniendo log_errors = On. Así sigues viendo los errores en los logs para depurar, pero el visitante nunca ve la estructura interna.

Defensa 5 — Capas extra: WAF y monitoreo

Después de corregir el código, añade capas de vigilancia. Un WAF (Web Application Firewall) — como ModSecurity con el conjunto de reglas OWASP CRS — frena patrones de ataque conocidos antes de que lleguen a la aplicación. No sustituye la corrección en el código, pero da una red de seguridad y tiempo de reacción.

Monta también un monitoreo simple para detectar intentos:

<?php
// Detección ligera de payloads sospechosos para alerta (NO como defensa 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");
        // dispara alerta (correo/Discord) y considera bloquear la IP
    }
}

Activa los logs de query de la base de datos en puntos sensibles y configura alertas para picos de error. Detectar temprano es la diferencia entre bloquear una IP y descubrir, semanas después, que medio servidor recibió ítems ilegales.

Puntos del sitio de MU que más son atacados

Prioriza la revisión en estos flujos, del más crítico al menos:

  1. Login — el blanco número uno; el bypass da acceso a cuentas.
  2. Registro — la inyección puede crear cuentas privilegiadas o corromper datos.
  3. Recuperación de contraseña — suele montar queries por correo/pregunta secreta.
  4. Ranking y listados — el ORDER BY dinámico es un clásico olvidado.
  5. Tienda / donación — toca saldo e ítems; el injection aquí se vuelve fraude directo.
  6. Panel del jugador — cambio de contraseña, correo, transferencias de personaje.

Revisa cada uno garantizando prepared statements, validación y el usuario de base de datos con privilegio mínimo.

Errores comunes y soluciones

Error / mala prácticaRiesgoCorrección
Concatenar $_POST directo en la queryBypass de login, robo de la baseMigrar a prepared statement
Confiar en que "un campo numérico es seguro"Injection por parámetro numéricoValidar entero y aun así parametrizar
ORDER BY $coluna del usuarioInjection en la cláusula ORDER BYAllowlist cerrada de columnas
El sitio conecta como sa/rootUn DROP inyectado lo borra todoUsuario dedicado con privilegio mínimo
Mostrar el error de SQL en pantallaFiltra la estructura (error-based)display_errors Off, mensaje genérico
Filtrar solo "palabras peligrosas"Sorteable, falsa sensación de seguridadAllowlist de formato, no blocklist
Contraseña comparada en texto planoRobo directo de credencialesHash fuerte y comparación por hash
Sin logs ni alertasAtaque descubierto demasiado tardeMonitorear payloads y picos de error

Lista de verificación de lanzamiento

  • Respaldo completo de la base de datos hecho antes de cualquier alteración
  • Todas las queries con datos del usuario migradas a prepared statements
  • Cláusulas ORDER BY / nombres de columna dinámicos con allowlist
  • Validación de tipo y formato en todos los campos de entrada
  • Usuario de la base dedicado, con privilegio mínimo (sin DROP/ALTER/GRANT)
  • Conexiones de lectura separadas de las de escritura cuando sea posible
  • display_errors apagado en producción, log_errors encendido
  • Mensajes de error genéricos para el usuario, detalles solo en el log
  • Contraseñas siempre con hash, nunca comparadas en texto plano
  • WAF (ej.: ModSecurity + OWASP CRS) evaluado como capa extra
  • Monitoreo de payloads sospechosos y alertas configurados
  • Flujos críticos revisados: login, registro, recuperación, ranking, tienda
  • Todo probado en entorno separado antes de subir a producción

El SQL injection es una amenaza seria, pero totalmente evitable. La base es siempre la misma: separa comando de datos con prepared statements, valida lo que entra, dale al sitio el mínimo de poder en la base y vigila lo que pasa. Aplicando estas capas, le quitas al atacante el atajo más fácil y proteges de verdad las cuentas y los ítems de tus jugadores.

Preguntas frecuentes

¿El SQL injection es realmente común en el sitio de MU Online?

Es una de las fallas más explotadas en servidores privados. Muchos sitios usan scripts antiguos que concatenan login y contraseña directo en la query. Un intruso logra burlar el login, robar cuentas, dar ítems o incluso borrar la base de datos por un formulario vulnerable.

¿Los prepared statements por sí solos resuelven todo?

Resuelven la mayor parte, pues separan comando de datos. Pero hay casos que exigen cuidado extra: nombres de tabla/columna dinámicos y cláusulas ORDER BY no aceptan bind y necesitan allowlist. Combina prepared statements con validación de entrada y privilegio mínimo.

¿Necesito cambiar todo mi sitio antiguo para protegerme?

No siempre, pero todo punto que toca la base de datos necesita ser revisado. Los formularios de login, registro, ranking, recuperación de contraseña y tienda son los blancos principales. Migra cada query a prepared statement y valida las entradas.

¿Un WAF sustituye la corrección del código?

No. Un WAF (firewall de aplicación) ayuda a frenar ataques conocidos y gana tiempo, pero es una capa extra, no la solución. La corrección real es en el código, con prepared statements y privilegio mínimo en el usuario de la base de datos.

¿Cómo sé si mi sitio ya fue atacado por SQL injection?

Busca patrones sospechosos en los logs (comillas, UNION SELECT, OR 1=1, comentarios -- en parámetros), cuentas creadas o alteradas sin origen, ítems que aparecieron de la nada y picos de error en la base de datos. Activa logs de query y alertas para reaccionar temprano.

GA
Editor de guías y builds

Gabriel cubre gameplay, builds de clases, PvP y progresión. Prueba cada estrategia en un servidor antes de publicar.

Sigue leyendo

Artículos relacionados