Ir al contenido

La subida de archivos que acaba siendo una shell

Validar el tipo mime no le dice qué es un archivo, y guardar las subidas bajo la raíz del sitio significa que el servidor ejecutará encantado lo que se validó mal.

5 min de lectura

Un formulario de subida es una ruta que deja a una persona anónima poner un archivo en su servidor. Casi todas las formas en que eso sale mal vienen de dos decisiones tomadas en la primera hora de construirlo.

Qué le dice de verdad la validación

$request->validate([
    'avatar' => 'required|file|mimes:jpg,png|max:2048',
]);

Esto es razonable y la gente confía demasiado en ello. mimes inspecciona el contenido del archivo en lugar de su nombre, lo cual es una mejora real sobre comprobar la extensión, pero "empieza con una cabecera PNG válida" y "es solo un PNG" no son la misma afirmación. Un archivo puede llevar una cabecera de imagen legítima y código PHP después, y aun así cumplir la regla.

Lo que hace eso inofensivo no es una validación mejor. Es que nada pueda ejecutar el archivo.

Dos reglas que son gratis y que merecen la pena igualmente:

'avatar' => 'required|image|dimensions:max_width=4000,max_height=4000|max:2048',

image es más estricta que mimes para subidas de imagen, y dimensions rechaza la bomba de descompresión: un archivo pequeño que se expande hasta algo que agota la memoria cuando su generador de miniaturas lo abre.

No se fíe nunca de getClientOriginalName() ni de getClientMimeType() para nada. Los dos los suministra el cliente. El nombre original está bien para guardarlo como etiqueta de visualización; no está bien para construir una ruta con él.

La decisión que de verdad importa: dónde aterriza

// mal para subidas de usuario
$request->file('avatar')->store('avatars', 'public');

El disco public es un enlace simbólico dentro de su raíz web. Cualquier cosa ahí la sirve directamente el servidor web, y si el servidor está configurado para ejecutar PHP en ese directorio - y muchos lo están, por defecto o por accidente - un archivo que pasó la validación es ahora una URL que ejecuta código.

Guarde las subidas en un disco privado:

$path = $request->file('avatar')->store('avatars');   // privado por defecto

y sírvalas a través de una ruta que compruebe quién pregunta:

public function show(Attachment $attachment)
{
    $this->authorize('view', $attachment);
 
    return Storage::download($attachment->path, $attachment->original_name);
}

Esa ruta cuesta un poco de rendimiento y compra dos cosas a la vez: nada es ejecutable, y el acceso está autorizado. El almacenamiento de objetos con URLs firmadas y caducables es la misma disposición con el ancho de banda movido fuera de su servidor.

Nombres, rutas y el directorio de arriba

Deje que el framework genere el nombre guardado. store() ya produce un nombre aleatorio, lo que elimina una categoría entera de problema: recorrido de rutas desde un nombre manipulado, archivos que se sobrescriben entre sí, y nombres que significan algo desafortunado para una shell.

Guarde el nombre original del usuario en una columna para mostrarlo. Son dos cosas distintas y confundirlas es de donde salen los fallos de recorrido.

Servir es donde queda el riesgo

SVG no es una imagen. Es un documento XML que puede contener script, y un navegador ejecuta ese script en el origen desde el que se sirvió. Si acepta SVG, o lo sanea con una biblioteca hecha para eso, o sirve los archivos de usuario desde un dominio aparte para que cualquier script que sobreviva corra donde no pueda alcanzar su cookie de sesión.

Ponga el tipo de contenido usted. Sirva un tipo guardado que usted decidió, no uno derivado del archivo en el momento de la descarga.

Fuerce una descarga donde pueda. Content-Disposition: attachment significa que el navegador guarda en lugar de renderizar, lo que neutraliza casi todo lo que un archivo hostil podría hacer en una página.

Quite los metadatos de las imágenes. Las fotografías llevan EXIF, y el EXIF lleva coordenadas GPS. Publicar sin procesar la foto subida por un usuario puede publicar dónde la tomó.

Los límites que nadie pone hasta que algo se rompe

Un límite de tamaño en la validación no es un límite de lo que llega a su servidor: eso lo deciden upload_max_filesize y post_max_size de PHP, y el servidor web tiene el suyo antes de que PHP vea nada. Póngalos los tres, a propósito, y asegúrese de que el error que recibe un usuario al superarlos sea un mensaje y no una página en blanco.

Limite la tasa de la ruta de subida. Un endpoint que acepta archivos sin límite es una forma de llenar su disco desde fuera, y un disco lleno se lleva por delante todo lo demás.

Análisis, cuando es usted un canal de distribución

Si los usuarios suben archivos que otros usuarios descargan, el riesgo ya no es solo suyo. Analice de forma asíncrona - un trabajo en cola después de la subida, con el archivo marcado como no disponible hasta que pase - para que un análisis lento no se quede dentro de la petición.

La versión corta

Validar con image y dimensions, guardar en privado con un nombre generado, servir a través de una ruta autorizada con un tipo de contenido que eligió usted, y no dejar nunca que las subidas aterricen donde el servidor web esté dispuesto a ejecutarlas.

Acierte con la ubicación de almacenamiento y la validación pasa a ser una comodidad en lugar de lo único que hay entre usted y una shell.

La ruta que sirve el archivo necesita la otra mitad: una comprobación de autorización, no un nombre imposible de adivinar. Las dos están en la lista que recorre una auditoría. Ninguna aparece en una revisión de código, porque el código que está ahí no tiene nada malo.

Preguntas relacionadas

¿Basta con la regla de validación mimes?
Es mejor que comprobar la extensión, porque inspecciona el archivo y no el nombre. Aun así no es suficiente por sí sola: un archivo puede llevar una cabecera de imagen válida y PHP después. La regla que de verdad impide la ejecución es guardar las subidas donde nada las vaya a ejecutar.
¿Dónde deberían guardarse las subidas?
Fuera de la raíz del sitio, o en almacenamiento de objetos, y servidas a través de una ruta que compruebe la autorización. El disco public es un enlace simbólico dentro de la raíz web, lo cual es correcto para recursos que quiere públicos y equivocado para cualquier cosa que haya subido un usuario.
¿Y los archivos SVG?
SVG es un documento, no una imagen: puede contener script, y un navegador lo ejecutará cuando el archivo se sirva desde su dominio. O rechace las subidas SVG, o sanéelas con una biblioteca dedicada, o sírvalas desde un dominio aparte para que cualquier script corra fuera de su origen.
¿Necesitamos análisis antivirus?
Donde los usuarios comparten archivos entre ellos, sí: en ese momento usted es un canal de distribución y el riesgo se traslada a quien descarga. Analice de forma asíncrona después de la subida y marque los archivos como no disponibles hasta que pasen.

← Volver a todos los artículos

Llamar+1 848 272 7583WhatsApp+90 850 308 5436Correoinfo@codefacture.comPágina de contacto