sábado, 15 de septiembre de 2012

Ya se PHP: ahora ¿qué?

Es muy común que gente sin experiencia o formación en el mundo de la programación aprenda por su cuenta PHP. Esto suele llevar a que cuando se quiere poner en práctica lo aprendido se haga un mal trabajo, acabando con un montón de código espagueti inmantenible, y plagado de agujeros de seguridad. De hecho, esto es uno de los factores que ha dado mala fama al lenguaje.

Por eso hoy voy a escribir unos cuantos consejos para quien esté en esa situación. Todos ellos yo mismo los aprendí tras cometer errores, y a veces por las malas. Se plantearán enfocados al caso de un programador que ya sabe PHP, y quiere hacer una pequeña web con un formulario de login y otro de registro de usuarios.

1. ¿Cómo acceder a la base de datos?

Instintivamente un novato tiende a usar las funciones como mysql_query, ya que es lo que se ve en muchos tutoriales anticuados, a pesar del aviso en rojo que indica que está desaconsejado. No es conveniente usar la API clásica (funciones mysql_*). ¿Por qué es mala idea? Porque es una API vieja, que ha sido reemplazada por MySQLi (o PDO), y es muy difícil de usar de forma segura. ¿Qué se debe usar entonces? Es indiferente. Con PDO la API es la misma para cualquier base de datos que se quiera usar, lo cual es una ventaja a tener en cuenta. Por otro lado, MySQLi tiene algunas funcionalidades extra y puede ser más rápido.

Una vez escogido el método, sin importar cual de ellos sea, llega la hora de hacer una query para, por ejemplo, ver si un usuario existe. Y el programador inexperto estará tentado de hacer algo como:
$query = "SELECT * FROM usuario WHERE login=$login";
Aunque se pueda hacer funcionar, no es buena idea concatenar datos del usuario en la query. Esta es la principal causa de inyecciones SQL, aún intentando escapar los datos proporcionados. ¿Entonces cómo debe hacerse? Utilizando prepared statements (sentencias preparadas). No solo por los posibles motivos de eficiencia, sino por la seguridad. Al pasar los parámetros de este modo, se evita tener que escaparlos y, con ello, el riesgo de una inyección SQL.

De todos modos, si tienes el servidor MySQL expuesto a internet y sin contraseña, te van a robar los datos en cuestión de minutos. Por eso es importante hacer "defensa en profundidad", y tomar las medidas de seguridad necesarias también en la propia base de datos:

2. ¿Qué precauciones requiere la base de datos?

Este tema daría para un curso entero, pero hay unas pocas precauciones especialmente importantes que merecen una mención especial.
  • La base de datos no debe ser visible desde internet. Por suerte, ésta es la configuración por defecto en la mayoría de casos, pero tener un firewall nunca está de más.
  • Es mejor utilizar un usuario con los mínimos privilegios posibles en el código PHP. Especialmente grave sería usar el usuario root y sin contraseña.
  • No se deben guardar las contraseñas de los usuarios. Es posible autenticar al usuario sin guardar su contraseña, utilizando un algoritmo hash. Lo ideal es usar bcrypt con un salt distinto en cada usuario. Hay decenas de páginas explicando cómo y por qué hacer esto así.
  • Las copias de seguridad de la base de datos son importantes, y deben custodiarse con sumo cuidado, ya que contienen datos confidenciales. Nunca deben dejarse guardadas en una carpeta pública del servidor.
Hay otros muchos consejos interesantes que se podrían añadir, pero esos cuatro seguramente sean los más relevantes para evitar incidentes de seguridad.

Con esto ya queda visto lo básico en cuanto a bases de datos. Lo siguiente es escribir el código que haga uso de los datos, pero esto también requiere cierto cuidado:

3. ¿Cómo organizar el código?

Sin conocer las "buenas costumbres" al respecto, se tiende a escribir el código PHP que accede a la base de datos mezclado en medio del HTML, duplicado en varias páginas. Se debe separar el acceso a datos del uso de los mismos para evitar ese caos.

La "receta" es sencilla: crear una clase (o módulo) a través de la cual acceder a la base de datos, con funciones para cada funcionalidad expuesta. Por ejemplo, la parte de usuarios se haría mediante una clase con las funciones:
// Parámetros: los datos de registro
// Devuelve: true si se registró correctamente, false en caso contrario
function registrar($login, $pass, $nombre, $email) { ... }

// Parámetros: los datos de inicio de sesión
// Devuelve: true si son correctos, false en caso contrario
function validar($login, $pass) { ... }

// Parámetros: el login del usuario objetivo
// Devuelve: un array asociativo con los datos del usuario (ej: $info['email'])
function info($login) { ... }
¿Por qué hacerlo así? Porque así el código está en un solo fichero, sin repetirse, y fácil de reutilizar en otras páginas que se desarrollen en el futuro, tan solo incluyendo el mismo fichero. Además facilita mucho leer el código cuando no está lleno de mensajes para el usuario, sino que únicamente hace su tarea.

Una vez tengamos las clases necesarias, ya estamos listos para utilizarlas. ¿Cómo juntamos esto con el HTML? Lo mejor es, sin duda, usar plantillas, ya que obligan a hacer la separación de forma correcta. En esto hay opiniones para gustos: algunos preferirán una solución tipo Smarty, y otros preferirán utilizar el propio PHP como motor de plantillas. En cualquier caso, la clave es mezclar lo menos posible la lógica de la página con su presentación.
(Recomiendo encarecidamente leer ejemplos con PHP como motor de plantillas, aunque se decida usar otra cosa.)

Pero aunque hagamos un uso seguro de la base de datos y nuestro código sea mantenible, a la hora de unir las distintas partes de nuestra página surgen nuevos retos:

4. ¿Cómo integrar las distintas páginas?

La duda más básica al respecto suele ser sobre cómo mantener la sesión abierta al cambiar de página. La mejor solución, casi siempre, es utilizando sesiones. Una vez autenticado el usuario, se guarda en la sesión su login, y en todas las páginas se comprueba en la propia sesión, sin usar la función de validar, si el usuario está autenticado. ¿Por qué hacerlo así? Porque es muy sencillo, seguro, y puede evitar errores.

En la comunicación entre páginas es importante tener en cuenta que todos los datos que pasemos a través del usuario (ej: datos POST o GET) pueden ser manipulados por un atacante, por lo que deben validarse. La sesión permite guardar en ella datos sin que el usuario pueda modificarlos, ya que tan solo contiene un ID, mientras que los datos se mantienen en el servidor.

La filosofía de considerar todos los datos del usuario como inseguros puede ahorrar muchos problemas. Por ejemplo, si se validan con Javascript los datos de un formulario, eso no exime de comprobarlos de nuevo desde el código PHP, ya que el usuario puede desactivar (o peor: modificar) el Javascript.

Finalmente, al tener varias páginas es imprescindible evitar la redundancia entre ellas. Si tenemos un menú en cada página, sería una locura repetirlo en cada fichero, ya que sería imposible de mantener. En estos casos, pueden utilizarse plantillas anidadas (ej: una plantilla con el menú, y en el resto de plantillas hacer un include de esa). No es aconsejable usar frames con esta finalidad, por muchas razones. Lo mismo sucede con los estilos: lo ideal es no tener información de estilo en el HTML, sino que se debe cargar de un fichero CSS que se puede compartir entre las distintas páginas.


Hasta aquí los consejos por hoy. Espero que a alguien le resulte de utilidad para aprender algo nuevo, o conocer nuevas opiniones. Cualquier comentario, crítica, o idea alternativa, es bienvenida en los comentarios.
Un saludo.