DEV Community

Isaac Ojeda
Isaac Ojeda

Posted on • Updated on

Hashids en ASP.NET Core: Protegiendo tus URLs (Parte 4)

Introducción

En esta entrega de la serie de artículos, abordaremos cómo realizar consultas seguras a los productos que previamente hemos creado. Es posible que no se haya dimensionado adecuadamente el problema al que se enfrentan las aplicaciones al utilizar "IDs secuenciales" que pueden ser modificados directamente desde la URL. En el contexto de la API que estamos desarrollando, esto puede no parecer un problema significativo, ya que se trata de un ejemplo. Sin embargo, en la vida real, este tipo de problemas son bastante comunes, aunque no siempre se les presta la debida atención.

Puedes encontrar el código fuente de este artículo en: MediatrValidationExample

Top 10 OWASP - A01 Broken Access Control

Uno de los riesgos de seguridad más comunes y destacados se encuentra en el punto principal del Top 10 de OWASP: A01 Broken Access Control. En el año 2017, esta vulnerabilidad ocupaba el quinto lugar, pero en 2021 ascendió al primer puesto. El A01 engloba diversas malas prácticas que pueden comprometer el control de acceso en nuestras aplicaciones, y en este artículo nos enfocaremos en resolver una de ellas:

Eludir las verificaciones de control de acceso al modificar la URL (alteración de parámetros o navegación forzada), el estado interno de la aplicación o la página HTML, o al utilizar una herramienta de ataque para modificar las solicitudes a la API.

Es bastante habitual en nuestras aplicaciones el uso de IDs Int32 secuenciales, una práctica que muchos programadores empleamos. Sin embargo, en términos de seguridad, esta elección plantea un desafío significativo. Imagina que tenemos una URL como esta: https://myapp.com/Payments/3232, donde supuestamente se muestra el pago con el ID 3232. Si somos lo suficientemente astutos, podríamos comenzar a explorar otros pagos realizados, incluso aquellos que no nos corresponden. Podríamos intentar con cualquier número secuencial que se nos ocurra.

De esta manera, si la URL no verifica los permisos de acceso, nuestra aplicación presenta una vulnerabilidad seria. Es cierto que en muchos casos se verifica la autorización, asegurándonos de que el usuario tenga los permisos adecuados para acceder al recurso. Sin embargo, ¿qué sucede si nuestro sitio es público, como YouTube? No debemos permitir que los usuarios intenten acceder a IDs predecibles, como 3232, o que intenten adivinar otros números por completo.
Aquí tienes una versión mejorada del texto:

HashIds: Genera IDs únicos y no secuenciales

Hashids es una herramienta que nos brinda una solución eficaz a este problema. Se trata de una pequeña librería que nos permite generar IDs de manera similar a lo que hace Youtube, a partir de números enteros.

Hashids ofrece las siguientes características:

  • Crea IDs únicos a partir de números enteros.
  • Genera IDs no secuenciales, lo que significa que no se pueden predecir como un número entero común.
  • Permite almacenar varios números enteros en un solo hash ID.
  • Es altamente personalizable, lo que significa que puedes utilizar los caracteres que desees en los IDs generados. ## Instalación

La instalación de esta biblioteca es sencilla. Solo necesitas ejecutar el siguiente comando:

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

¿Cómo utilizarlo?

El funcionamiento de Hashids es muy simple. Puedes crear un objeto Hashids con una "salt" que elijas:

var hashids = new Hashids("esta es mi salt");
var hash = hashids.Encode(12345);
Enter fullscreen mode Exit fullscreen mode

Esto te devolverá un hash como este:

NkK9
Enter fullscreen mode Exit fullscreen mode

Para recuperar el ID original a partir del hash, puedes hacer lo siguiente:

var hashids = new Hashids("esta es mi salt");
var numbers = hashids.Decode("NkK9");
Enter fullscreen mode Exit fullscreen mode

Obtendrás una matriz que contiene los números enteros originales:

[ 12345 ]
Enter fullscreen mode Exit fullscreen mode

Si deseas explorar más sobre las funciones y características de Hashids, puedes visitar su repositorio en GitHub.

Integrándolo con Queries y Comandos

Dado que este artículo es una continuación de la implementación de CQRS con MediatR, ahora vamos a integrar Hashids con los queries y comandos que hemos desarrollado hasta el momento.

Para facilitar su uso, crearemos métodos de extensión que permitirán la conversión directa de enteros (int) a cadenas (string) ya hasheadas y viceversa.

Nota 💡: Es importante tener en cuenta que, aunque hemos optado por utilizar estos métodos de extensión por su simplicidad y su compatibilidad con AutoMapper, es posible que existan alternativas que permitan una implementación aún más eficiente. Siempre es válido explorar otras opciones para lograr los mejores resultados en tus proyectos.

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

Este conjunto de métodos de extensión nos facilitará realizar acciones como IntProperty.ToHashId(). Los métodos de extensión son una herramienta valiosa en este contexto.

El "Salt" se utiliza para generar los hashes a partir de ese valor, como si fuera una "llave". Es importante tener en cuenta que no se trata de un hash en el sentido tradicional, ya que es reversible.

GetProductsQuery

Para implementar esta funcionalidad, es necesario modificar el tipo de dato de ProductId a string. Además, ajustaremos el perfil de mapeo (Mapping Profile) para llevar a cabo 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

Recordemos que estamos utilizando AutoMapper y estos tutoriales están estructurados de forma secuencial. Si aún no has trabajado con AutoMapper, te recomendamos revisar estos tutoriales para comprender su funcionamiento.

Si ejecutamos esta configuración utilizando Swagger, obtendremos una respuesta similar a la siguiente:

[
  {
    "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 puedes observar, hemos dejado de utilizar IDs secuenciales, y en cuanto al rendimiento, podemos estar seguros de que esta implementación no añade una carga significativa.

Estos IDs, aunque los números enteros puedan ser secuenciales, se convierten en hashes que resultan imposibles de adivinar (al menos, a menos que se conozca el Salt).

GetProductQuery

En este caso, vamos a buscar un producto por su ID, por lo que es necesario realizar la conversión de vuelta a un número entero.

Nota: El ProductId del request debe convertirse a string. Siempre puedes visitar el repositorio asociado a este artículo para confirmar la forma en que se realiza todo el proceso.

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

La implementación de estos métodos de extensión facilita enormemente todas estas operaciones.

Podemos consultar el producto con el ID "Wxoz2o" y debería encontrarse sin problemas.

Image description

Conclusión

Si bien podría parecer repetitivo tener que realizar la codificación y decodificación continuamente, esta es una opción que puedes considerar en tus proyectos. La aplicación de Hashids resulta efectiva para proteger las URLs, y aunque no es necesario aplicarla en todos los recursos de tus aplicaciones, puede ser una solución útil cuando sea necesario garantizar la seguridad de los datos.

Referencias

Top comments (1)

Collapse
 
edd profile image
NSysX • Edited

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.