Manual de Burp Suite Community Edition

Capítulo 11 · Carrito de la compra y checkout

Serie: Burp Suite desde cero  |  unfantasmaenelsistema.com

Con el Bloque C cerrado ya dominas las herramientas de Burp: Proxy, Repeater, Intruder, Decoder, Comparer, Sequencer y la BApp Store. El Bloque D deja de centrarse en la herramienta para centrarse en el objetivo, y no hay mejor sitio para empezar que el flujo que todas las tiendas online comparten: añadir productos al carrito, ajustar cantidades, y completar un pedido. Es un recorrido largo -carrito, dirección, método de envío, pago, confirmación- y cada paso genera su propia petición HTTP con su propia superficie de prueba.

Qué aprenderás en este capítulo

Qué peticiones dispara añadir un producto y cambiar su cantidad en el carrito · Cómo Repeater revela un límite de negocio (máximo 5 unidades) que la interfaz nunca menciona · Que una cantidad negativa se acepta sin validar, y qué significa eso en la práctica · El recorrido completo del checkout -dirección, método de envío, pago- y qué guarda cada paso · Qué contiene realmente la petición final que cierra la compra, y sobre todo qué NO contiene

1. Añadir productos al carrito

Cada clic en "Add to Basket" genera un `POST /api/BasketItems/` con el id del producto, el id de tu carrito (`BasketId`) y la cantidad. Con dos productos añadidos, la propia página del carrito ya deja ver algo curioso:

Carrito con dos productos. Fíjate en el total: 6.970000000000001¤

Carrito con dos productos. Fíjate en el total: 6.970000000000001¤

6.970000000000001 no es un error de Burp, es un clásico de JavaScript

El total del carrito lo calcula el propio navegador sumando `precio × cantidad` de cada línea en punto flotante -el típico problema de `0.1 + 0.2` en JavaScript, no un fallo de seguridad. Pero el detalle importa para lo que viene después: si el total se calcula en el cliente, ¿se vuelve a calcular en el servidor a la hora de cobrar, o se confía en lo que manda el navegador? Ese es exactamente el tipo de pregunta que Burp permite responder, y la sección 4 de este capítulo la responde.

Cambiar la cantidad con los botones + / - del carrito genera un `PUT /api/BasketItems/{id}` con el nuevo valor en el cuerpo. Esta es la petición que vamos a usar como conejillo de indias en la siguiente sección:

PUT /api/BasketItems/9 con {

PUT /api/BasketItems/9 con {"quantity":3} en el cuerpo, y la fila completa del BasketItem en la respuesta

2. Repeater revela lo que la interfaz no cuenta

Envía esa petición PUT a Repeater (clic derecho → Send to Repeater, o Ctrl+R) y prueba a subir la cantidad muy por encima de lo que cualquier persona pediría a mano -por ejemplo, 9999 unidades de zumo de manzana.

400 Bad Request:

400 Bad Request: "You can order only up to 5 items of this product." Un límite que la interfaz jamás muestra

La aplicación sí valida esto en el servidor, y el mensaje de error es un regalo: revela un límite de negocio (máximo 5 unidades por producto) que no aparece en ningún sitio de la interfaz. Sin Repeater, ese límite solo se descubriría clicando el botón + cinco veces y viendo qué pasa en el sexto clic -con Repeater se descubre en un envío, y además queda documentado el mensaje exacto que devuelve el servidor.

El límite superior está cubierto. ¿Y por debajo de cero? Cambia el cuerpo a una cantidad negativa y reenvía:

200 OK con

200 OK con "quantity":-59999 guardado tal cual. El límite de 5 solo mira hacia arriba

Un límite a medias es casi tan malo como no tener límite

El servidor comprueba que la cantidad no sea excesivamente alta, pero no comprueba que sea razonable en absoluto -acepta sin más una cantidad negativa de casi sesenta mil unidades. En una tienda real, una cantidad negativa suele restar del total en vez de sumar, lo que abre la puerta a que un carrito con un producto muy negativo y otro positivo cierre con un importe final absurdamente bajo, o incluso negativo. Curiosamente, la interfaz del carrito ni se entera: sigue mostrando la cantidad anterior porque el componente de Angular no vuelve a pedir el carrito al servidor tras la edición, así que la única forma de ver lo que de verdad quedó guardado es, otra vez, mirar la respuesta en Burp.

3. El proceso de checkout paso a paso

Con el carrito arreglado (cantidad de vuelta a un valor sensato) y pulsando Checkout, Juice Shop encadena cuatro pantallas, cada una respaldada por su propia petición REST. La primera es la dirección de envío:

Alta de una nueva dirección: POST /api/Addresses/ con país, nombre, teléfono, código postal, dirección y ciudad

Alta de una nueva dirección: POST /api/Addresses/ con país, nombre, teléfono, código postal, dirección y ciudad

Cada dirección guardada recibe un id propio, exactamente como cada BasketItem del apartado anterior -y exactamente el mismo tipo de dato que conviene vigilar de cara al capítulo 13, dedicado en profundidad a las direcciones como candidato clásico a IDOR. Con la dirección elegida, toca el método de envío:

Tres velocidades de envío con precio distinto: 0.99¤ a 1 día, 0.50¤ a 3 días, gratis a 5 días

Tres velocidades de envío con precio distinto: 0.99¤ a 1 día, 0.50¤ a 3 días, gratis a 5 días

Y después el pago, donde aparece un detalle que vale la pena anotar para más adelante: la opción Add a coupon.

Pantalla de pago: tarjeta guardada, saldo del monedero, y un campo de cupón de descuento

Pantalla de pago: tarjeta guardada, saldo del monedero, y un campo de cupón de descuento

Un campo de texto libre que aplica un descuento es una invitación a fuerza bruta

Un código de cupón es, ni más ni menos, una contraseña corta que da dinero en vez de acceso. El mismo Intruder del capítulo 8 que se usó para probar contraseñas sirve igual de bien para probar una lista de cupones típicos -y la única diferencia respecto a un login es que aquí ni siquiera hace falta acertar con una cuenta ajena para que el hallazgo tenga valor.

El último paso antes de pagar es un resumen que junta dirección, método de pago y contenido del carrito en una sola pantalla -la última oportunidad de revisar antes de que la compra sea irreversible:

Resumen final: 8.96¤ en artículos + 0.50¤ de envío = 9.46¤ de total

Resumen final: 8.96¤ en artículos + 0.50¤ de envío = 9.46¤ de total

4. La petición que realmente hace la compra

Todo lo anterior son formularios que van guardando datos sueltos -direcciones, tarjetas- sin comprometer nada. La compra se cierra con una única petición, y su cuerpo es mucho más corto de lo que cabría esperar:

POST /rest/basket/6/checkout con {

POST /rest/basket/6/checkout con {"couponData":"bnVsbA==","orderDetails":{"paymentId":"7","addressId":"7","deliveryMethodId":"2"}}

Fíjate en lo que no lleva esta petición: ni un solo producto, ni una cantidad, ni un importe. Solo tres identificadores -de pago, de dirección y de método de envío- más un campo de cupón que, incluso vacío, viaja codificado en Base64 (`bnVsbA==` decodifica a la palabra `null`, no a una cadena vacía real). El servidor reconstruye el pedido entero a partir del `BasketId` de la URL, leyendo de su propia base de datos qué productos y qué cantidades tiene guardados ese carrito en ese momento.

Por qué esto conecta con la sección 2

Que el checkout no reciba el importe del cliente es, en general, una buena práctica -evita el fallo obvio de mandar `{"total":0.01}` y que el servidor se lo crea sin más. Pero también significa que cualquier manipulación que sobreviva en el carrito llega intacta al pedido final: la cantidad negativa de la sección 2, si se hubiera dejado así en lugar de corregirla, se habría calculado en el total real de la compra exactamente igual que cualquier otra cantidad, con el servidor confiando ciegamente en lo que tiene guardado. Un límite que solo mira hacia arriba dejó de ser un detalle curioso para convertirse en el origen de un pedido con importe manipulado.

Pantalla final:

Pantalla final: "Thank you for your purchase!" con el desglose completo del pedido ya cerrado

Conclusión

El carrito y el checkout no son una pantalla, son media docena de peticiones encadenadas -cada añadido, cada cambio de cantidad, cada paso del checkout- y cada una con su propia superficie: límites de cantidad que solo se descubren forzándolos, direcciones con id propio candidatas a IDOR, un campo de cupón que es una contraseña disfrazada, y una petición final que revela qué datos decide confiar el servidor y cuáles reconstruye por su cuenta.

El capítulo siguiente se queda en el terreno de los formularios pero cambia de objetivo: cuestionarios y validación del lado servidor frente al lado cliente, con un vistazo a un fallo clásico de las APIs REST mal diseñadas -el mass assignment.

unfantasmaenelsistema.com | Seguridad, sistemas y todo lo que no debería funcionar (pero funciona)