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.
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 defectoy 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.
