10 Trucos avanzados de PHP

En Smashing Magazine han publicado una recopilación de 10 trucos avanzados en php:

Índice
  1. Usar una chuleta de Inyecciones SQL.
  2. Conocer la diferencia entre los operadores de comparación.
  3. Atajar los "else".
  4. Usar str_replace() antes que ereg_replace() y preg_replace().
  5. Usar Operadores Ternarios.
  6. Usar Memcached.
  7. Usar un Framework.
  8. Usar el operador supresion (@) correctamente.
  9. Usar isset() en vez de strlen().

Usar una chuleta de Inyecciones SQL.

Este consejo en particular es sólo un enlace a un recurso útil sin discusión sobre cómo usarlo. Estudiar varias permutaciones de un ataque específico puede ser útil, pero es mejor que dediques tu tiempo a aprender a protegerte contra él. Además, la seguridad de las aplicaciones Web es mucho más importante que la inyección SQL. XSS (Cross-Site Scripting) y CSRF (Cross-Site Request Forgeries), por ejemplo, son al menos tan comunes y peligrosos.

Podemos ofrecer un contexto muy necesario, pero como no queremos centrarnos demasiado en un ataque, primero daremos un paso atrás. Todos los desarrolladores deben estar familiarizados con las buenas prácticas de seguridad, y las aplicaciones deben diseñarse teniendo en cuenta estas prácticas. Una regla fundamental es nunca confiar en los datos que recibes de otro lugar. Otra regla es escapar los datos antes de enviarlos a otra parte. Combinadas, estas reglas pueden simplificarse para constituir un principio básico de seguridad: entrada de filtro, salida de escape (FIEO).

La causa principal de la inyección SQL es un fallo en la salida de escape. Más específicamente, es cuando la distinción entre el formato de una consulta SQL y los datos utilizados por la consulta SQL no se mantiene cuidadosamente. Esto es común en las aplicaciones PHP que construyen consultas de la siguiente manera:

<?php

$query = "SELECT *
FROM users
WHERE name = '{$_GET['name']}'";

?>

En este caso, el valor de $_GET['nombre'] es proporcionado por otra fuente, el usuario, pero no se filtra ni se escapa.

Escapar preserva los datos en un nuevo contexto. El énfasis en la salida de escape es un recordatorio de que los datos usados fuera de su aplicación Web necesitan ser escapados, de lo contrario podrían ser malinterpretados. Por el contrario, el filtrado garantiza que los datos sean válidos antes de ser utilizados. El énfasis en el filtrado de entradas es un recordatorio de que los datos que se originan fuera de su aplicación Web necesitan ser filtrados, porque no se puede confiar en ellos.

Asumiendo que estamos usando MySQL, la vulnerabilidad de la inyección SQL puede ser mitigada escapando del nombre con mysql_real_escape_string(). Si el nombre también se filtra, hay una capa adicional de seguridad. (La implementación de múltiples capas de seguridad se llama "defensa en profundidad" y es una muy buena práctica de seguridad.) El siguiente ejemplo muestra cómo filtrar la entrada y la salida de escape, con convenciones de nombres utilizadas para la claridad del código:

<?php

// Initialize arrays for filtered and escaped data, respectively.
$clean = array();
$sql = array();

// Filter the name. (For simplicity, we require alphabetic names.)
if (ctype_alpha($_GET['name'])) {
$clean['name'] = $_GET['name'];
} else {
// The name is invalid. Do something here.
}

// Escape the name.
$sql['name'] = mysql_real_escape_string($clean['name']);

// Construct the query.
$query = "SELECT *
FROM users
WHERE name = '{$sql['name']}'";

?>

Aunque el uso de convenciones de nomenclatura puede ayudarle a mantenerse al día con lo que se ha filtrado y lo que no se ha filtrado, así como con lo que se ha escapado y lo que no se ha escapado, un enfoque mucho mejor es utilizar sentencias preparadas. Afortunadamente, con PHP Data Objects, los desarrolladores de PHP tienen una API universal para el acceso a los datos que soporta las sentencias preparadas, incluso si la base de datos subyacente no lo hace.

Recuerde, las vulnerabilidades de inyección SQL existen cuando la distinción entre el formato de una consulta SQL y los datos utilizados por la consulta SQL no se mantiene cuidadosamente. Con las sentencias preparadas, puede trasladar esta responsabilidad a la base de datos proporcionando el formato de la consulta y los datos en pasos distintos:

<?php

// Provide the query format.
$query = $db->prepare('SELECT *
FROM users
WHERE name = :name');

// Provide the query data and execute the query.
$query->execute(array('name' => $clean['name']));

?>

La página del manual de la PDO proporciona más información y ejemplos. Las sentencias preparadas ofrecen la mayor protección contra la inyección SQL.

Conocer la diferencia entre los operadores de comparación.

Es importante conocer las diferencias entre los distintos operadores de comparación para evitar que nos generen problemas.

Si utiliza strpos() para determinar si una subcadena existe dentro de una cadena (devuelve FALSE si no se encuentra la subcadena), los resultados pueden ser engañosos:

<?php
$authors = 'Chris & Sean';

if (strpos($authors, 'Chris')) {
    echo 'Chris is an author.';
} else {
    echo 'Chris is not an author.';
}
?>

Debido a que la subcadena Chris ocurre al principio de Chris & Sean, strpos() devuelve correctamente 0, indicando la primera posición en la cadena. Debido a que la expresión condicional trata esto como un booleano, se evalúa a FALSE, y la condición falla. En otras palabras, parece que Chris no es un autor, ¡pero lo es!

Esto se puede resolver con una comparacion estricta:

<?php
if (strpos($authors, 'Chris') !== FALSE) {
    echo 'Chris is an author.';
} else {
    echo 'Chris is not an author.';
}
?>

Atajar los "else".

Este consejo consiste en inicializar siempre las variables antes de utilizarlas. Considere una declaración condicional que determina si un usuario es un administrador basado en el nombre de usuario:

<?php

if (auth($username) == 'admin') {
    $admin = TRUE;
} else {
    $admin = FALSE;
}
?>

Esto parece bastante seguro, porque es fácil de comprender de un vistazo. Imagine un ejemplo un poco más elaborado que establece variables para el nombre y el correo electrónico también, por conveniencia:

<?php
if (auth($username) == 'admin') {
    $name = 'Administrator';
    $email = '[email protected]';
    $admin = TRUE;
} else {
    /* Get the name and email from the database. */
    $query = $db->prepare('SELECT name, email
                           FROM   users
                           WHERE  username = :username');
    $query->execute(array('username' => $clean['username']));
    $result = $query->fetch(PDO::FETCH_ASSOC);
    $name = $result['name'];
    $email = $result['email']; 
    $admin = FALSE;
}
?>

Debido a que $admin siempre se establece explícitamente como VERDADERO o FALSO, todo está bien, pero si un desarrollador añade más tarde un elseif, hay una oportunidad de olvidar:

<?php
if (auth($username) == 'admin') {
    $name = 'Administrator';
    $email = '[email protected]';
    $admin = TRUE;
} elseif (auth($username) == 'mod') {
    $name = 'Moderator';
    $email = '[email protected]';
    $moderator = TRUE;
} else {
    /* Get the name and email. */
    $query = $db->prepare('SELECT name, email
                           FROM   users
                           WHERE  username = :username');
    $query->execute(array('username' => $clean['username']));
    $result = $query->fetch(PDO::FETCH_ASSOC);
    $name = $result['name'];
    $email = $result['email']; 
    $admin = FALSE;
    $moderator = FALSE;
}
?>

Si un usuario proporciona un nombre de usuario que activa la condición elseif, $admin no se inicializa. Esto puede conducir a un comportamiento no deseado o, peor aún, a una vulnerabilidad de seguridad. Además, ahora existe una situación similar para $moderator, que no se inicializa en la primera condición.

Al inicializar primero $admin y $moderator, es fácil evitar este escenario por completo:

<?php
if (auth($username) == 'admin') {
    $name = 'Administrator';
    $email = '[email protected]';
    $admin = TRUE;
} elseif (auth($username) == 'mod') {
    $name = 'Moderator';
    $email = '[email protected]';
    $moderator = TRUE;
} else {
    /* Get the name and email. */
    $query = $db->prepare('SELECT name, email
                           FROM   users
                           WHERE  username = :username');
    $query->execute(array('username' => $clean['username']));
    $result = $query->fetch(PDO::FETCH_ASSOC);
    $name = $result['name'];
    $email = $result['email']; 
    $admin = FALSE;
    $moderator = FALSE;
}
?>

Independientemente de lo que haga el resto del código, ahora está claro que $admin es FALSO a menos que esté explícitamente establecido en otra cosa, y lo mismo ocurre con $moderator. Esto también es un indicio de otra buena práctica de seguridad, que es la de fracasar de forma segura. Lo peor que puede pasar como resultado de no modificar $admin o $moderator en cualquiera de las condiciones es que alguien que es un administrador o moderador no sea tratado como tal.

Si quieres atajar algo, y te sientes un poco decepcionado de que nuestro ejemplo incluya otra cosa, tenemos un consejo adicional que puede que te interese. No estamos seguros de que se pueda considerar un atajo, pero esperamos que sea útil.

Considere una función que determine si un usuario está autorizado a ver una página en particular:

<?php

function authorized($username, $page) {
    if (!isBlacklisted($username)) {
        if (isAdmin($username)) {
            return TRUE;
        } elseif (isAllowed($username, $page)) {
            return TRUE;
        } else {
            return FALSE;
        }
    } else {
        return FALSE;
    }
}

?>

Este ejemplo es bastante simple, porque sólo hay tres reglas a considerar: los administradores siempre tienen acceso; los que están en la lista negra nunca tienen acceso; e isAllowed() determina si alguien más tiene acceso. (Existe un caso especial cuando un administrador está en la lista negra, pero esa es una posibilidad poco probable, así que lo estamos ignorando aquí.) Usamos funciones para las reglas para mantener el código simple y para enfocarnos en la estructura lógica.

Hay muchas maneras de mejorar este ejemplo. Si desea reducir el número de líneas, un condicional compuesto puede ayudar:

<?php

function authorized($username, $page) {
    if (!isBlacklisted($username)) {
        if (isAdmin($username) || isAllowed($username, $page)) {
            return TRUE;
        } else {
            return FALSE;
        }
    } else {
        return FALSE;
    }
}

?>

De hecho, puede reducir toda la función a un solo condicional:

<?php

function authorized($username, $page) {
    if (!isBlacklisted($username) && (isAdmin($username) || isAllowed($username, $page)) {
        return TRUE;
    } else {
        return FALSE;
    }
}

?>

Por último, esto puede reducirse a una sola devolución de variables:

<?php

function authorized($username, $page) {
    return (!isBlacklisted($username) && (isAdmin($username) || isAllowed($username, $page));
}

?>

Si su objetivo es reducir el número de líneas, se acabó. Sin embargo, tenga en cuenta que estamos usando isBlacklisted(), isAdmin(), y isAllowed() como marcadores de posición. Dependiendo de lo que implica hacer estas determinaciones, reducir todo a un compuesto condicional puede no ser tan atractivo.

Esto nos lleva a nuestra propina. Una devolución sale inmediatamente de la función, por lo que si regresa lo antes posible, puede expresar estas reglas de forma muy sencilla:

<?php

function authorized($username, $page) {

    if (isBlacklisted($username)) {
        return FALSE;
    }

    if (isAdmin($username)) {
        return TRUE;
    }

    return isAllowed($username, $page);
}
?>

Esto usa más líneas de código, pero es muy simple y poco impresionante (estamos más orgullosos de nuestro código cuando es el menos impresionante). Y lo que es más importante, este enfoque reduce la cantidad de contexto con el que debe mantenerse al día. Por ejemplo, tan pronto como haya determinado si el usuario está en la lista negra, puede olvidarse de él con seguridad. Esto es particularmente útil cuando su lógica es más complicada.

Usar str_replace() antes que ereg_replace() y preg_replace().

Odiamos sonar despectivos, pero este consejo demuestra el tipo de malentendido que lleva al mismo mal uso que está tratando de prevenir. Es una verdad obvia que las funciones de cadena son más rápidas en la coincidencia de cadenas que las funciones de expresión regulares, pero el intento del autor de extraer un corolario de esto falla miserablemente:

Si estás usando expresiones regulares, entonces ereg_replace() y preg_replace() serán mucho más rápidos que str_replace().

Debido a que str_replace() no soporta la concordancia de patrones, esta declaración no tiene sentido. La elección entre funciones de cadena y funciones de expresión regular se reduce a cuál es la adecuada para el propósito, no cuál es la más rápida. Si necesita hacer coincidir un patrón, utilice una función de expresión regular. Si necesita hacer coincidir una cadena, utilice una función de cadena.

Usar Operadores Ternarios.

El beneficio del operador ternario es discutible (sólo hay uno, por cierto). Aquí hay una línea de código de una auditoría que realizamos recientemente:

<?php
$host = strlen($host) > 0 ? $host : htmlentities($host);
?>

El autor realmente quiere escapar de $host si la longitud de la cadena es mayor que cero, pero en su lugar accidentalmente hace lo contrario. ¿Un error fácil de cometer? Tal vez. ¿Fácil de pasar por alto durante una auditoría de código? Por supuesto que sí. La decisión no necesariamente mejora el código.

El operador ternario puede estar bien para las líneas simples, los prototipos y las plantillas, pero creemos firmemente que una sentencia condicional ordinaria es casi siempre mejor. PHP es descriptivo y prolijo. Creemos que el código también debería serlo.

Usar Memcached.

El acceso al disco es lento. El acceso a la red es lento. Las bases de datos suelen utilizar ambos.

La memoria es rápida. El uso de una caché local evita la sobrecarga de la red y el acceso al disco. Combine estas verdades y obtendrá memcached, un "sistema de almacenamiento en caché de objetos de memoria distribuida" desarrollado originalmente para la plataforma de blogs LiveJournal basada en Perl.

Si su aplicación no está distribuida en varios servidores, probablemente no necesite memcached. Los enfoques de almacenamiento en caché más sencillos (por ejemplo, la serialización de datos y su almacenamiento en un archivo temporal) pueden eliminar una gran cantidad de trabajo redundante en cada solicitud. De hecho, este es el tipo de fruta que tenemos en cuenta cuando ayudamos a nuestros clientes a afinar sus aplicaciones.

Una de las formas más fáciles y universales de almacenar datos en la memoria es utilizar los asistentes de memoria compartida en APC, un sistema de almacenamiento en caché desarrollado originalmente por nuestro colega George Schlossnagle. Considere el siguiente ejemplo:

<?php
$feed = apc_fetch('news');

if ($feed === FALSE) {
    $feed = file_get_contents('http://example.org/news.xml');
    // Store this data in shared memory for five minutes.
    apc_store('news', $feed, 300);
}

// Do something with $feed.

?>

Con este tipo de almacenamiento en caché, no tiene que esperar en un servidor remoto para enviar los datos de alimentación para cada solicitud. En este ejemplo se incurre en cierta latencia -hasta cinco minutos-, pero ésta puede ser ajustada en tiempo real según lo requiera su aplicación.

Usar un Framework.

Todas las decisiones tienen consecuencias. Apreciamos los marcos de trabajo - de hecho, los principales desarrolladores detrás de CakePHP y Solar trabajan con nosotros en OmniTI - pero el uso de uno no hace que lo que estás haciendo sea mejor por arte de magia.

En diciembre, nuestro colega Paul Jones escribió un artículo para PHP Advent titulado The Framework as Franchise, en el que compara los marcos con las franquicias de negocios. Se refiere a una sugerencia de Michael Gerber de su libro "The E-Myth Revisited":

Gerber señala que para manejar un negocio exitoso, el empresario necesita actuar como si fuera a vender su negocio como un prototipo de franquicia. Es la única manera en que el dueño del negocio puede hacer que éste opere sin que él se vea involucrado personalmente en cada decisión.

Este es un buen consejo. Ya sea que esté utilizando un marco de trabajo o definiendo sus propios estándares y convenciones, es importante considerar el valor desde la perspectiva de los futuros desarrolladores.

Aunque nos encantaría darles una verdad universal, extender esta idea para sugerir que un marco siempre es apropiado no es algo que estemos dispuestos a hacer. Si nos preguntas si deberías usar un marco, la mejor respuesta que podemos dar es: "Depende".

Usar el operador supresion (@) correctamente.

Intente siempre evitar el uso del operador de supresión de errores.

El operador @ es bastante lento y puede ser costoso si necesita escribir código teniendo en cuenta el rendimiento.

La supresión de errores es lenta. Esto se debe a que PHP cambia dinámicamente el error_reporting a 0 antes de ejecutar la instrucción suprimida, y luego lo vuelve a cambiar inmediatamente. Esto es caro.

Peor aún, el uso del operador de supresión de errores dificulta la búsqueda de la causa raíz de un problema.

El artículo anterior utiliza el siguiente ejemplo para apoyar la práctica de asignar una variable por referencia cuando se desconoce si se ha establecido $albus:

<?php

$albert =& $albus;

?>

Aunque esto funciona - por ahora - confiar en un comportamiento extraño e indocumentado sin un buen entendimiento de por qué funciona es una buena manera de introducir errores. Dado que $albert está asignado a $albus por referencia, las futuras modificaciones de $albus también modificarán $albert.

Una solución mucho mejor es usar isset(), con frenos:

<?php

if (!isset($albus)) {
    $albert = NULL;
}

?>

Asignar $albert a NULL es lo mismo que asignarlo a una referencia inexistente, pero ser explícito mejora enormemente la claridad del código y evita la relación referencial entre las dos variables.

Si hereda código que utiliza excesivamente el operador de supresión de errores, tenemos un consejo adicional para usted. Hay una nueva extensión de PECL llamada Scream que desactiva la supresión de errores.

Usar isset() en vez de strlen().

<?php

if (isset($username[5])) {
    // The username is at least six characters long.
}

?>

Cuando se tratan las cadenas como matrices, cada carácter de la cadena es un elemento de la matriz. Al determinar si existe un elemento en particular, se puede determinar si el string tiene al menos tantos caracteres de longitud. (Tenga en cuenta que el primer carácter es el elemento 0, por lo que $username[5] es el sexto carácter en $username.)

La razón por la que esto es un poco más rápido que strlen() es complicada. La explicación simple es que strlen() es una función, e isset() es una construcción del lenguaje. En general, llamar a una función es más caro que usar una construcción de lenguaje.

Sin duda, buenos consejos a tener en cuenta cuando nos ponemos a programar en php.

¡Haz clic para puntuar esta entrada!
(Votos: 1 Promedio: 3)

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir