Proteger, controlar y restringir acceso a imágenes usando PHP y Apache

Introducción

En este artículo mostraré cómo podemos proteger nuestras imágenes o fotos usando PHP para que sólo en casos específicos se puedan ver. Usaremos la configuración de Apache para restringir el acceso (para que no puedan acceder a ejemplo.com/imagenes/imagen_secreta.png). PHP será utilizado para leer ese archivo y mostrarlo sólo si se debería.

Preparando archivos para trabajar

En nuestro entorno de trabajo de PHP (en htdocs si estás usando xampp) crearemos una carpeta llamada “probar_fotos“. Dentro de ella crearemos un archivo index.php y otra carpeta llamada img.  Dentro de esta última carpeta pondremos algunas imágenes de nuestra elección (no importa el formato ni calidad) y un archivo llamado .htaccess.

En mi caso, el directorio se ve así:

Configurando htaccess

En primera instancia vamos a denegar cualquier acceso a la carpeta en donde tenemos nuestras imágenes. Para ello nos servirá el archivo que pusimos ahí dentro. Simplemente le diremos al servidor que deniegue cualquier acceso.

Antes de modificar el archivo, si navegamos a localhost/probar_fotos/img podemos ver todas las imágenes:

De esta manera todos podrían verlas. Y eso no es lo que queremos. Por ello, en el archivo .htaccess escribiremos:

Deny from all

Guardamos cambios y volvemos a la ruta. Ahora nos encontraremos con esto:

Listo, ya tenemos nuestra carpeta protegida. Nota: incluso si el usuario supiera el nombre de la imagen y quisiera acceder a ella a través de localhost/probar_fotos/img/st.jpg no podría; saldría esto:

Con esto hemos terminado de configurar a Apache. Ahora es el turno de PHP.

Leyendo archivos con PHP

Como PHP leerá los archivos sin pasar a través del servidor, no importa que éstos estén protegidos por Apache.

El algoritmo es simple: debemos saber cómo se llama la imagen que queremos leer, comprobar si existe, mandar los encabezados (para que el navegador interprete los datos como imagen) y finalmente mandar la imagen. Por ejemplo, yo tengo una imagen llamada st.jpg, y para mostrarla hago lo siguiente en index.php:

<?php
$rutaImagen = __DIR__ . "/img/st.jpg";
$informacionImagen = getimagesize($rutaImagen);
header("Content-type: {$informacionImagen['mime']}");
readfile($rutaImagen);
?>

En la primera línea declaro la ruta de la imagen. Luego, obtengo su información (para saber si es png, jpg, etcétera) y mando los encabezados. Finalmente hago uso de readfile para leer la imagen y mandarla a través del búfer de salida. Podríamos usar file_get_contents pero ésta función también carga la imagen a memoria, cosa que no queremos, ya que sólo queremos mandarla directamente, sin hacerle modificación alguna.

Si ahora voy a localhost/probar_fotos/index.php veré lo siguiente:

Podemos ver que la imagen ha sido mostrada.

Leyendo archivos sólo si se tiene permiso

Arriba mostramos la imagen a cualquier usuario, cosa que es insegura como si no protegiéramos nada. Pero para restringir el acceso a determinados usuarios dependerá de cómo sea nuestra aplicación. Por ejemplo, si sólo los usuarios logueados pueden verlas, sería más o menos así:

<?php
session_start();
if($_SESSION["logueado"] === true){

  $rutaImagen = __DIR__ . "/img/st.jpg";
  $informacionImagen = getimagesize($rutaImagen);
  header("Content-type: {$informacionImagen['mime']}");
  readfile($rutaImagen);
}
?>

Si en cambio dependiera de un nivel de acceso almacenado en sesión, sería algo así:

<?php
session_start();
if($_SESSION["permiso_usuario"] >= 5){ #Suponiendo que el nivel de permisos para ver imágenes es 5

  $rutaImagen = __DIR__ . "/img/st.jpg";
  $informacionImagen = getimagesize($rutaImagen);
  header("Content-type: {$informacionImagen['mime']}");
  readfile($rutaImagen);
}
?>

Todo esto cambia dependiendo de cómo sea nuestro software. Más abajo omitiré esta comprobación del usuario, y me centraré en cómo mostrar archivos de diversas maneras. Queda en manos del desarrollador proteger el acceso a determinados clientes.

Mostrando imágenes según nombre

Ahora vamos a hacer que se muestre cualquier imagen si es que sabemos su nombre, pero sólo si existe y si su extensión es .jpg. Para ello leeremos el nombre almacenado en la variable $_GET. Modificaré el código así:

<?php
if(isset($_GET["nombre"])){ #Comprobar si está definida la variable
	$nombreImagen = $_GET["nombre"];
	$rutaImagen = __DIR__ . "/img/$nombreImagen.jpg"; #Concatenar nombre con .jpg
	if(file_exists($rutaImagen)){ #Comprobar si el archivo existe
		$informacionImagen = getimagesize($rutaImagen);
		header("Content-type: {$informacionImagen['mime']}");
		readfile($rutaImagen);
	}
}
?>

Ahora iré a localhost/probar_fotos/index.php?nombre=the-good-dinosaur y veré lo siguiente:

Y si accedo a localhost/probar_fotos/index.php?nombre=st veré lo siguiente:

De esta manera podríamos leer cualquier imagen, aunque nunca debemos de hacerlo de esta manera, porque cualquier usuario malicioso podría ir probando nombres de imágenes y viendo si existen o no. Además, podría navegar a otros directorios.

Esto nos lleva a otro método que detallo más abajo.

Mostrando imágenes por nombre y expresión regular

Este método se parece mucho al de arriba, sólo que ahora comprobaremos el nombre con una expresión regular. Básicamente sólo permitiremos imágenes cuyo nombre esté compuesto por letras y números o guiones, por un punto y por una extensión que puede ser png, jpg o jpeg. Si no se da ningún nombre, la expresión no coincide o no existe el archivo, simplemente indicamos que no se encontró la imagen.

<?php
if(isset($_GET["nombre"])){ #Comprobar si está definida la variable
	$nombreImagen = $_GET["nombre"];
	$expresionRegular = '/^[a-z0-9A-Z-]*\.(?:png|jpg|jpeg)$/';
	if(preg_match($expresionRegular, $nombreImagen) === 1){
		$rutaImagen = __DIR__ . "/img/$nombreImagen"; #Concatenar nombre con __DIR__
		if(file_exists($rutaImagen)){ #Comprobar si el archivo existe
			$informacionImagen = getimagesize($rutaImagen);
			header("Content-type: {$informacionImagen['mime']}");
			readfile($rutaImagen);
		}else exit("Imagen no encontrada");
	}else exit("Imagen no encontrada");
}else exit("Imagen no encontrada");
?>

Ahora puedo navegar a localhost/probar_fotos/index.php?nombre=the-good-dinosaur.jpg y ver la imagen. Si pongo un nombre raro como ../../ veré el error:

Por otro lado, si el nombre de la imagen coincide con la expresión regular pero no existe, veremos lo mismo:

Conclusión

Puede que parezca que en realidad no estamos haciendo nada, pero sí que lo estamos haciendo. De esta manera podemos proteger el acceso a imágenes para algunos usuarios; podemos hacer todo tipo de cosas, todo depende de los requerimientos de nuestra aplicación, pero la base es la misma.

Referencias

Stack Overflow – Show image using file_get_contents

Stack Overflow – Deny direct access to all .php files except index.php