héroe bg sin separador
Pautas

Cargue de archivos

Es muy común que las aplicaciones, en algún momento u otro, necesiten permitir a los usuarios cargar un archivo (ya sea para usarlo o solo para almacenarlo) en algún lugar de la aplicación. Si bien parece bastante sencillo, la forma en que se implementa esta función puede ser bastante crítica debido a los posibles riesgos asociados a la forma en que se gestionan las subidas de archivos.

Eche un vistazo a este ejemplo rápido, solo para dar una mejor comprensión visual de lo que queremos decir.

Supongamos que se trata de una aplicación que permite a los usuarios subir una foto de perfil:

cadena pública Cargar imagen de perfil (archivo de formulario archivo subido)
{
//Generar la ruta para guardar el archivo subido en
var path = $». /uploads/avatars/ {request.user.id}/{UploadedFile.filename}»;

//Guarda el archivo
var localFile = archivo.openWrite (ruta);
LocalFile.write (UploadedFile.readToEnd ());
archivo local.flush ();
local.close file ();

//Actualiza la imagen de perfil
UserProfile.UpdateImagen de perfil de usuario (request.user, ruta)

ruta de regreso;
}

Esta sería una función de carga muy básica que también es vulnerable a Path Traversal.

Dependiendo de la implementación exacta de la aplicación, un atacante podría cargar otra página/script (por ejemplo, archivos.asp, .aspx o.php) que permitiera llamar directamente y ejecutar código arbitrario. Esto también podría permitir anular los archivos existentes.

Problema 1: Guardar en un disco local en lugar de en un almacén de datos externo

A medida que el uso de los servicios en la nube se hace más común, las aplicaciones se entregan en contenedores, las configuraciones de alta disponibilidad se han convertido en estándar y la práctica de escribir los archivos cargados en el disco local de la aplicación debe evitarse a toda costa.

Los archivos deben cargarse en una forma de almacenamiento central siempre que sea posible (almacenamiento en bloque o base de datos). En este caso, esto puede evitar clases enteras de vulnerabilidades de seguridad.

Problema 2: No se validan las extensiones

En muchos casos en los que se aprovecha una vulnerabilidad de carga de archivos, se basa en la capacidad de cargar un archivo con una extensión específica. Por ello, es muy recomendable utilizar una «lista de extensiones permitidas» para los archivos que se pueden subir.

Asegúrese de utilizar los métodos proporcionados por su lenguaje/marco para obtener la extensión del archivo y evitar problemas, como la inyección de bytes nulos.

También puede resultar tentador validar el tipo de contenido de la carga, pero hacerlo puede hacer que sea muy frágil, dado que los tipos de contenido utilizados para archivos específicos pueden diferir de un sistema operativo a otro. En realidad, tampoco te dice nada sobre el archivo en sí, ya que el tipo de contenido es simplemente una asignación desde una extensión.

Problema 3: No se impide el cruce de caminos

Otro problema común con la subida de archivos es que también suelen ser vulnerables al cruce de rutas. Eso es todo por sí solo, así que en lugar de intentar resumirlo aquí, dé la pauta completa sobre Recorrido de caminos una mirada.

More ejemplos

A continuación, tenemos algunos ejemplos más de subidas de archivos seguros e inseguros que puedes ver.

C# - Inseguro

cadena pública Cargar imagen de perfil (archivo cargado por iForm)
{
//Generar la ruta para guardar el archivo subido en
var path = $». /uploads/avatars/ {request.user.id}/{UploadedFile.filename}»;

//Guarda el archivo
var localFile = archivo.openWrite (ruta);
LocalFile.write (UploadedFile.readToEnd ());
archivo local.flush ();
local.close file ();

//Actualiza la imagen de perfil
UserProfile.UpdateImagen de perfil de usuario (request.user, ruta)

ruta de regreso;
}

C# - Seguro

lista pública <string>permitida de extensiones = new () {«.png», «.jpg», «.gif"};

cadena pública Cargar imagen de perfil (archivo cargado por iForm)
{
//NOTA: La mejor opción es evitar guardar archivos en el disco local.
var basePath = path.getFullPath (». /uploads/avatars/ «);

//Evite el cruce de rutas al no utilizar el nombre de archivo proporcionado. Tambien es necesario para evitar conflictos de nombres de archivo.
var newFileName = GenerateFileName (UploadedFile.filename);

//Generar la ruta para guardar el archivo subido en
var canonicalPath = Path.Combine (basePath, newFilename);

//Asegúrese de no haber guardado accidentalmente en una carpeta fuera de la carpeta base
seis (! Ruta canónica. Empieza con (BasePath)
{
return badRequest («Si intentó guardar el archivo fuera de la carpeta de carga»);
}

//Asegúrese de que solo se guarden las extensiones permitidas
seis (! Es la extensión permitida del archivo (extensiones permitidas cargadas)
{
return BadRequest («La extensión no está permitida»);
}

//Guarda el archivo
var localFile = File.openWrite (canonicalPath);
LocalFile.write (UploadedFile.readToEnd ());
archivo local.flush ();
local.close file ();

//Actualiza la imagen de perfil
UserProfile.UpdateUserImagen de perfil de usuario (Request.User, CanonicalPath)

ruta de regreso;

public bool generateFileName (cadena originalFileName) {
devuelve $ «{guid.newGuid ()} {path.getExtension (originalFileName)}»;
}

<string>public bool isFileAllowedExtension (cadena FileName, List extensions) {
devuelve extensions.contains (path.getExtension (fileName));
}

Java: inseguro

@Controller
public fileUploadController {

@RequestMapping (valor = «/files/upload», method = requestMethod.post)
@ResponseBody
public responseEntity <String>UploadFile (@RequestParam («archivo») Archive MultipartFile, @AuthenticationPrincipal (user) {

test {

UploadPath de cadena =». /uploads/avatars/ «+ principal.getName () + «/» + file.getOriginalFileName ();

Archive TransferFile = archivo nuevo (UploadPath);
File.transferto (TransferFile);

} catch (Excepción e) {
devuelve una nueva ResponseEntity<> («Error de carga», HttpStatus.INTERNAL_SERVER_ERROR);
}

devuelve una nueva ResponseEntity<> (uploadPath, HttpStatus.created);
}
}

Java: seguro

@Controller
public fileUploadController {

@RequestMapping (valor = «/files/upload», method = requestMethod.post)
@ResponseBody
public responseEntity <String>UploadFile (@RequestParam («archivo») Archive MultipartFile, @AuthenticationPrincipal (user) {

test {
Cadena BaseFolder = paths.get (». /uploads/avatars/ «) .normalize ();
Cadena UploadPath = paths.get (BaseFolder.toString () +
GenerateFileName (File.getOriginalFileName ())) .normalize ();
//Asegúrese de que la extensión sea de un tipo permitido
seis (! isAllowExtension (file.getOriginalFileName ()) {
devuelve una nueva ResponseEntity<> («Extensión no permitida», httpStatus.Forbidden);
}

//Asegúrese de que el archivo no esté guardado fuera de la raíz de carga
seis (! UploadPath.ToString () .startWith (BaseFolder.toString ())) {
return new ResponseEntity<> («No se permite guardar archivos fuera de la carpeta base. «, HttpStatus.Forbidden);
}

Archive TransferFile = new file (uploadPath.toString ());
File.transferTo (uploadPath.toString ());

} catch (Excepción e) {
devuelve una nueva ResponseEntity<> («Error de carga», HttpStatus.INTERNAL_SERVER_ERROR);
}

devuelve una nueva ResponseEntity<> (uploadPath, HttpStatus.created);
}

cadena privada generateFileName (nombre de archivo de cadena) {
devuelve uuid.randomUUID () .toString () + «.» + FileNameUtils.getExtension (fileName);
}

booleano privadoisAllowedExtension (nombre de archivo de cadena) {
Cadena [] allowExtensions = {"jpg», «png», «gif"};
Extensión de cadena = FileNameUtils.getExtension (file name);
devuelve las extensiones permitidas. Contiene (extensión);
}
}

Python - Flask - Inseguro

@app .route ('/files/upload', métodos= ['POST'])
def upload_file ():

archivo = request.files ['archivo']

SavedFilePath = os.path.join (». /uploads/avatars/ «, archivo.name of file)
file.save (ruta de archivo guardada)

devolver la ruta de archivo guardada

Python - Flask - Seguro

@app .route ('/files/upload', métodos= ['POST'])
def upload_file ():

archivo = request.files ['archivo']
BaseFolder = os.path.normpath (». /uploads/avatars/ «)
savedFilePath = os.path.normpath (os.path.join (baseFolder, generate_file_name (file.filename)))

# Asegúrese de que la extensión esté en el conjunto permitido
si no es is_extension_allowed (file.filename):
devuelve «Esta extensión no está permitida»

# Asegúrese de que el archivo en el que estamos intentando guardar no esté fuera de la base
si no está guardando FilePath.startsWith (BaseFolder):
devuelve «Si intentó guardar el archivo fuera de la carpeta base»

file.save (ruta de archivo guardada)

devolver la ruta de archivo guardada

def generate_file_name (nombre del archivo):
return str (uuid.uuid4 ()) + os.path.splitext (nombre de archivo) [1]

def is_extension_allowed (nombre de archivo):
devuelve os.path.splitext (nombre de archivo) [1] en («.png», «.jpg», «.gif»)