legacy-lens · radiografia de un sistema legacy

Tienda legacy (ejemplo)

Codigo: examples/tienda-legacy/app · Esquema: examples/tienda-legacy/schema.sql · Generado: 2026-10-29 19:36 UTC
73/100
Doloroso
13 tablas
57 columnas
- filas sin conteos
- tamano en disco
12 archivos PHP
234 lineas
12 hallazgos altos
14 hallazgos medios

Indice de dolor de migracion

Esquema 83
  • 2 de 13 tablas sin PK
  • cero foreign keys declaradas
  • 7 relaciones solo por nombre de columna
  • nombres hostiles: 4
  • tablas MyISAM: 6
  • IDs como texto: 5
  • fechas como texto: 4
  • charset viejo: 7
  • tablas gemelas: 1
Datos 15
  • Sin conteos de filas (dump sin datos): el volumen no se pudo medir
Codigo 74
  • 7 de 12 archivos tocan SQL
  • 234 lineas de PHP
Seguridad 92
  • 13 hallazgos de severidad alta
  • 3 de severidad media
Modernidad 85
  • sin composer.json
  • sin namespaces
  • sin tests

Plan de accion

  1. 1
    Reescribir el acceso a datos

    mysql_* no existe en PHP 7+: esa capa se escribe de nuevo, no se parchea. (10 casos)

    alto
  2. 2
    Pasar las consultas a prepared statements

    Se arregla de a una, con tests que cubran el caso real. (3 casos)

    medio
  3. 3
    Rotar el usuario de base y externalizar la configuracion

    El password de la base esta en un archivo versionado. (3 casos)

    medio
  4. 4
    Revisar y encapsular los comandos de sistema

    Cada exec/system es una puerta de ejecucion remota si toca el request. (2 casos)

    alto
  5. 5
    Rotar credenciales y moverlas a variables de entorno

    Una credencial que estuvo en el codigo ya esta comprometida, aunque se borre. (2 casos)

    medio
  6. 6
    Documentar las relaciones por convencion y decidir el modelo

    Sin FK, el ORM nuevo no puede inferir nada: hay que escribir el mapa a mano antes de migrar. (1 caso)

    alto
  7. 7
    Sacar el input del usuario del SQL

    Es inyeccion directa: cualquier migracion que copie estas consultas hereda el agujero. (1 caso)

    alto
  8. 8
    Rehashear contrasenas con password_hash()

    Se migra en el login: hash nuevo la primera vez que entra cada usuario. (1 caso)

    medio

Esquema 11 reglas

Alta

Esquema sin claves foraneas

1 caso

La integridad se sostiene solo en el codigo: cualquier consulta puede dejar huerfanos.

  • esquema0 FOREIGN KEY declaradas en toda la base
Media

Relaciones por convencion de nombre

7 casos

Columnas tipo id_cliente que apuntan a otra tabla sin FK: hay que documentarlas antes de migrar.

  • esquemaproductos.id_categoria -> categorias
  • esquemapedidos.id_cliente -> clientes
  • esquemapedido_items.id_pedido -> pedidos
  • esquemapedido_items.id_producto -> productos
  • esquemapagos.id_pedido -> pedidos
  • esquemalog_accesos.id_usuario -> usuarios
  • esquemaenvios.id_pedido -> pedidos
Media

Tablas MyISAM (sin transacciones)

6 casos

MyISAM no soporta transacciones ni FK: migrar sin atomicidad es una ruleta.

  • esquemapedidos
  • esquemapedido_items
  • esquemapagos
  • esquemalog_accesos
  • esquemaproveedor
  • esquemareportes_mensuales
Media

Columnas que apuntan a tablas inexistentes

5 casos

El nombre promete una relacion que no existe: datos muertos o tablas que se borraron.

  • esquemaclientes.id_localidad (sin tabla destino)
  • esquemapagos.id_metodo (sin tabla destino)
  • esquemapagos.id_factura (sin tabla destino)
  • esquemausuarios.id_rol (sin tabla destino)
  • esquemaenvios.id_transporte (sin tabla destino)
Media

Identificadores guardados como texto

5 casos

Los IDs en VARCHAR no comparan igual que un INT: joins lentos y orden lexicografico.

  • esquemaclientes.id_localidad (varchar(10))
  • esquemapagos.id_metodo (varchar(6))
  • esquemausuarios.id_rol (varchar(4))
  • esquemalog_accesos.id_usuario (varchar(20))
  • esquemaenvios.id_transporte (varchar(8))
Media

Nombres de tabla o columna hostiles

4 casos

Espacios, puntos, mayusculas o palabras reservadas obligan a escapar todo el tiempo.

  • esquemaclientes.`password`
  • esquemareportes_mensuales.`total.'MES 1'`
  • esquemareportes_mensuales.`total.'MES 2'`
  • esquemareportes_mensuales.`Order`
Media

Fechas guardadas como texto

4 casos

Comparar y ordenar fechas en VARCHAR devuelve resultados incorrectos.

  • esquemaproductos.fecha_alta (varchar(10))
  • esquemapedidos.fecha (varchar(10))
  • esquemapagos.fecha_pago (varchar(10))
  • esquemaenvios.fecha_envio (varchar(10))
Media

Tablas sin clave primaria

2 casos

Sin PK no hay forma segura de actualizar una fila ni de idempotear una migracion.

  • esquemapedido_items
  • esquemalog_accesos
Baja

Charset distinto de utf8mb4

7 casos

latin1/utf8 de 3 bytes rompe emojis y acentos al pasar a un framework moderno.

  • esquemapedidos (latin1)
  • esquemapedido_items (latin1)
  • esquemapagos (latin1)
  • esquemausuarios (utf8)
  • esquemalog_accesos (latin1)
  • esquemaproveedor (latin1)
  • esquemareportes_mensuales (latin1)
Baja

Columnas ENUM

1 caso

Un ENUM es un catalogo escondido en el DDL: agregar un valor es un ALTER TABLE.

  • esquemaproductos.activo (enum('S','N'))
Baja

Tablas con nombres casi iguales

1 caso

Suelen ser copias o restos de una migracion anterior: confirmar cual esta viva.

  • esquemaproveedores ~ proveedor

Seguridad 12 reglas

Alta

Credenciales de base en el codigo

3 casos

El acceso a la base esta escrito en un .php versionado.

  • config\database.php:3define('DB_HOST', '***');
  • config\database.php:4define('DB_USER', '***');
  • config\database.php:5define('DB_PASSWORD', '***');
Alta

Ejecucion de comandos del sistema

2 casos

Si el argumento toca datos del request, es ejecucion remota de comandos.

  • backup.php:7system("mysqldump " . $base . " > /tmp/backup.sql");
  • backup.php:10$hoy = shell_exec('date +%Y%m%d');
Alta

Credencial hardcodeada

2 casos

Credenciales en el codigo: hay que rotarlas y pasarlas a variables de entorno.

  • config\database.php:8$password = '***';
  • config\database.php:9$api_key = '***';
Alta

Consulta SQL armada con datos del request

1 caso

Un parametro del usuario entra directo en el SQL: inyeccion segura.

  • clientes.php:13$sql = "SELECT id, nombre, email FROM clientes WHERE nombre LIKE '%" . $_GET['buscar'] . "%'";
Alta

Hash debil para contrasenas

1 caso

MD5/SHA1 no sirven para contrasenas: password_hash() con bcrypt o argon2.

  • login.php:8$hash = md5($clave);
Alta

eval() / assert() con codigo dinamico

1 caso

Ejecucion de codigo arbitrario: no tiene traduccion directa a un framework.

  • pago.php:10$total = eval($regla);
Alta

include/require con ruta variable

1 caso

Inclusion remota de archivos (LFI/RFI) y dependencias invisibles para el IDE.

  • export.php:6include $pagina . '.php';
Alta

Acceso a archivos con datos del request

1 caso

Lectura o borrado de archivos controlado por el usuario.

  • export.php:15readfile($_GET['descargar']);
Alta

preg_replace con modificador /e

1 caso

El modificador /e ejecuta codigo: se elimino en PHP 7.

  • admin.php:8$salida = preg_replace('/\{(\w+)\}/e', '$plantilla', $_GET['texto']);
Media

Token predecible (md5/uniqid)

1 caso

Tokens de sesion o de recuperacion generados con funciones predecibles.

  • login.php:8$hash = md5($clave);
Media

unserialize() sobre datos externos

1 caso

Deserializacion insegura: preferir JSON.

  • pago.php:17$carrito = unserialize(base64_decode($_POST['carrito']));
Media

Redireccion con variable (open redirect)

1 caso

El destino del redirect lo decide el request.

  • reportes.php:24header("Location: $volver");

Codigo 9 reglas

Alta

Funciones mysql_* (extintas desde PHP 7)

10 casos

No existen en PHP 7+: el archivo no corre sin reescribir el acceso a datos.

  • login.php:11$resultado = mysql_query($sql);
  • login.php:12if ($resultado && mysql_num_rows($resultado) > 0) {
  • login.php:13$fila = mysql_fetch_assoc($resultado);
  • pedidos.php:13while ($fila = mysql_fetch_assoc($rs)) {
  • productos.php:16while ($fila = mysql_fetch_array($rs)) {
  • reportes.php:10$fila = mysql_fetch_assoc($rs);
  • utils.php:19mysql_query($sql);
  • config\database.php:14$link = mysql_connect(DB_HOST, DB_USER, DB_PASSWORD);
  • config\database.php:15mysql_select_db(DB_NAME, $link);
  • includes\db.php:10$res = mysql_query($sql, $link);
Alta

Query sin sentencia preparada

3 casos

El SQL se arma con interpolacion de variables en vez de parametros.

  • login.php:11$resultado = mysql_query($sql);
  • utils.php:19mysql_query($sql);
  • includes\db.php:10$res = mysql_query($sql, $link);
Media

SQL con variables interpoladas

2 casos

El string de la consulta se construye concatenando variables.

  • reportes.php:8$sql = "SELECT anio, `total.'MES 1'`, `total.'MES 2'` FROM reportes_mensuales WHERE anio = $anio";
  • utils.php:18$sql = "DELETE FROM pedidos WHERE id = $id";
Media

extract() / variables variables

2 casos

Variables que aparecen de la nada: imposible de tipar o testear.

  • clientes.php:7extract($_GET);
  • clientes.php:8extract($_POST);
Media

each() (extinta desde PHP 8.0)

1 caso

Se elimino en PHP 8: reemplazar por foreach.

  • pedidos.php:14list($id, $datos) = each($fila);
Media

Errores silenciados

1 caso

Los errores se ocultan en vez de corregirse; migrar asi es a ciegas.

  • includes\db.php:12error_reporting(0);
Baja

session_start() repetido

6 casos

Sesiones abiertas en cada include: sintoma de que no hay bootstrap.

  • admin.php:3session_start();
  • clientes.php:3session_start();
  • clientes.php:4session_start();
  • utils.php:3session_start();
  • includes\db.php:4session_start();
  • includes\db.php:5session_start();
Baja

utf8_encode / utf8_decode (deprecadas en 8.2)

3 casos

Deprecadas: migrar a mb_convert_encoding o a utf8mb4 de punta a punta.

  • productos.php:23<td><?php echo utf8_decode($f['nombre']); ?></td>
  • utils.php:7return utf8_encode(trim($texto));
  • includes\db.php:20return utf8_encode($valor);
Baja

Funciones eliminadas o deprecadas

2 casos

No existen en PHP 8: cada una rompe el archivo donde aparece.

  • admin.php:11$formatear = create_function('$n', 'return number_format($n, 2, ",", ".");');
  • utils.php:12return money_format('%.2n', $valor);

Tablas 13

TablaFilasColumnas MotorPKFK
clientes 0* 6 InnoDB si 0
categorias 0* 2 InnoDB si 0
productos 0* 6 InnoDB si 0
pedidos 0* 6 MyISAM si 0
pedido_items 0* 4 MyISAM no 0
pagos 0* 6 MyISAM si 0
usuarios 0* 5 InnoDB si 0
log_accesos 0* 4 MyISAM no 0
proveedores 0* 3 InnoDB si 0
proveedor 0* 2 MyISAM si 0
config 0* 3 InnoDB si 0
reportes_mensuales 0* 5 MyISAM si 0
envios 0* 5 InnoDB si 0

Relaciones inferidas 12

ColumnaDestinoTipo
productos.id_categoria categorias convencion
pedidos.id_cliente clientes convencion
pedido_items.id_pedido pedidos convencion
pedido_items.id_producto productos convencion
pagos.id_pedido pedidos convencion
log_accesos.id_usuario usuarios convencion
envios.id_pedido pedidos convencion
clientes.id_localidad <sin tabla> huerfana
pagos.id_metodo <sin tabla> huerfana
pagos.id_factura <sin tabla> huerfana
usuarios.id_rol <sin tabla> huerfana
envios.id_transporte <sin tabla> huerfana