DEV Community

Isaac Ojeda
Isaac Ojeda

Posted on

[Parte 4] ASP.NET: URLs seguros con HashIds

Introducción

En esta continuación de las series de Posts veremos como consultar de manera segura los Productos que anteriormente habíamos hecho.

Tal vez no se dimensione bien el problema que se exponen las aplicaciones cuando usan IDs secuenciales y estos son modificables desde un URL. En la API que estamos desarrollando pasa, pero realmente no es ningún problema porque estamos hablando de un ejemplo.

En la vida real este tipo de problemas existen y no muchos se dan cuenta.

Código fuente de este post: DevToPosts/MediatrValidationExample at post-part4 · isaacOjeda/DevToPosts (github.com)

Top 10 OWASP - A01 Broken Access Control

Una vulnerabilidad que suele ser muy común se incluye en este punto del Top 10 de OWASP: A01 Broken Access Control.

Este punto en el 2017 se encontraba en el quinto lugar, 2021 pasó a estar en el primer lugar. El A01 describe muchas malas prácticas que pueden romper el control de acceso de nuestra aplicación, pero el que venimos a solucionar es el siguiente:

Bypassing access control checks by modifying the URL (parameter tampering or force browsing), internal application state, or the HTML page, or by using an attack tool modifying API requests.

Es muy común que en nuestras aplicaciones usemos IDs Int32 secuenciales, yo siempre lo hago. Pero nos encontramos con un problemón aquí cuando hablamos de seguridad.

Si tenemos un URL como este: https://myapp.com/Payments/3232 donde supongamos que puedo ver el pago con ID 3232. Si soy lo suficientemente listo puedo comenzar a explorar otros pagos realizados de los cuales no me corresponde ver ninguno. Podría intentar con cualquier número secuencial que se me venga a la mente.

Así, si ese URL no verifica accesos, tenemos una vulnerabilidad grave en la aplicación.

Es real que se verifica comprobando accesos, ver si el usuario que intenta accesar tiene permisos para consultar el recurso. ¿Pero qué tal si es un sitio público? como Youtube? No debemos dar la oportunidad de que intenten ingresar IDs reconocibles como 3232 e intentar con otros que son 100% adivinables.

HashIds

Hashids viene a solucionarnos este problema.

Hashids es una pequeña librería que nos permite generar IDs al estilo Youtube a partir de números enteros.

Hashids tiene lo siguiente:

  • Crea IDs únicos a partir de un número

  • Genera IDs no secuenciales, no se pueden adivinar como un número entero

  • Puedes guardar varios números enteros en un mismo hash id

  • Personalizable, se pueden usar los carácteres que se deseen

Instalación

Para instalar este paquete ejecutamos:

dotnet add package hashids.net
Enter fullscreen mode Exit fullscreen mode

¿Cómo se usa?

Su funcionamiento es muy simple

var hashids = new Hashids("this is my salt");
var hash = hashids.Encode(12345);
Enter fullscreen mode Exit fullscreen mode

Nos regresa el este hash:

NkK9
Enter fullscreen mode Exit fullscreen mode

Para obtener de vuelta el ID original:

var hashids = new Hashids("this is my salt");
numbers = hashids.Decode("NkK9");
Enter fullscreen mode Exit fullscreen mode

Nos regresará un array, ya que se pueden guardar varios números enteros en un solo hash.

[ 12345 ]
Enter fullscreen mode Exit fullscreen mode

Si quieres ver más sobre sus otras funcionalidades, visita su repositorio.

Integrandolo con los Queries y Comandos

Como esto es una continuación del uso de CQRS con MediatR, vamos a integrarlo con los queries y comandos que actualmente tenemos.

Para facilitar el uso, crearemos unos métodos de extensión para convertir directamente los int a string ya hasheados y vicebersa.

Helpers/AppHelpers.cs

using HashidsNet;

namespace MediatrValidationExample.Helpers;

public static class AppHelpers
{
    public const string HashIdsSalt = "s3cret_s4lt";

    public static string ToHashId(this int number) =>
        GetHasher().Encode(number);

    public static int FromHashId(this string encoded) =>
        GetHasher().Decode(encoded).FirstOrDefault();

    private static Hashids GetHasher() => new(HashIdsSalt, 8);
}
Enter fullscreen mode Exit fullscreen mode

Esta clase helper nos ayudará a hacer cosas tipo IntProperty.ToHashId(), los métodos de extensión son una chulada.

El Salt se usa para que los hashes se generen a partir de ese salt, como si fuera la "llave". Realmente no es un hash de verdad, porque es reversible.

Nota 💡: Estamos usando métodos de extensión para usarlo de forma más sencilla en la proyección de AutoMapper. Sin AutoMapper se podría usar directo.

GetProductsQuery

Para emplear esto, hay que hacer que ProductId sea ahora un string y modificamos el Mapping Profile para que haga esta conversión:

public class GetProductsQueryProfile : Profile
{
    public GetProductsQueryProfile() =>
        CreateMap<Product, GetProductsQueryResponse>()
            .ForMember(dest =>
                dest.ListDescription,
                opt => opt.MapFrom(mf => $"{mf.Description} - {mf.Price:c}"))
            .ForMember(dest =>
                dest.ProductId,
                opt => opt.MapFrom(mf => mf.ProductId.ToHashId()));

}
Enter fullscreen mode Exit fullscreen mode

Recuerden que estamos usando AutoMapper y estos tutoriales llevan una secuencia, te recomiendo que los revises si no lo has hecho.

Si ejecutamos con Swagger:

[
  {
    "productId": "Wxoz2omz",
    "description": "Product 01",
    "price": 16000,
    "listDescription": "Product 01 - $16,000.00"
  },
  {
    "productId": "kwoYX31v",
    "description": "Product 02",
    "price": 52200,
    "listDescription": "Product 02 - $52,200.00"
  }
]
Enter fullscreen mode Exit fullscreen mode

Como ves aquí, ya no estamos usando IDs secuenciales y si te preocupa el performance, hay vídeos en Youtube que lo ponen a prueba y no es un overhead del que nos tenemos que preocupar.

Estos IDs aunque en número son secuenciales, en HashId son imposibles de adivinar (al menos que te sepas el Salt).

GetProductQuery

Aquí hacemos lo mismo, aquí vamos a buscar un producto por ID, entonces tenemos que convertirlo de vuelta a un entero.

Nota: ProductId del request hay que convertirlo a un string. Siempre puedes visitar el repositorio de este post para confirmar la forma en que se está haciendo todo.

var product = await _context.Products.FindAsync(request.ProductId.FromHashId());

if (product is null)
{
    throw new NotFoundException(nameof(Product), request.ProductId);
}

return _mapper.Map<GetProductQueryResponse>(product);
Enter fullscreen mode Exit fullscreen mode

Los métodos de extensión nos ayudan a que todo esto sea demasiado fácil.

Podemos consultar el producto con Wxoz2o y debería de encontrarlo sin problema

Image description

Conclusión

Podría volverse repetitivo el siempre tener que estar haciendo el encoding/decoding, pero es una opción que puedes considerar en tus proyectos.

Para proteger los URLs funciona muy bien, tal vez no lo vas a hacer en todos tus entities, pero cuando sea necesario, ya sabemos cómo podemos solucionar esta vulnerabilidad.

Referencias

Discussion (1)

Collapse
edd profile image
NSysX • Edited on

Gracias por esta serie de aportes, si usas un tipo record en lugar de una clase para un DTO en el mapping tienes que cambiar el ForMemeber x el .ForCtorParam.