# Author → stored XSS in wp-admin: unescaped sub-size filename from attachment metadata breaks out of the `src` attribute in `get_media_item()`

Author stores unsanitized sub_sizes[].file via REST finalize_item; unescaped src attribute in get_media_item() yields stored XSS in an Administrator's wp-admin media popup.

- Payout: hidden
- Severity: critical
- Program: WordPress
- Found by: jakubk
- Disclosed: 2026-08-28
- Write-up quality: 9/10
- Original: https://hackerone.com/reports/3931771
- Library page: https://bugbountylibrary.com/r/h1%3A3931771
- Tags: area:web domain:web impact:ato impact:privesc kind:bounty platform:hackerone sev:critical tech:php tech:rest technique:chained technique:source-review vuln:xss

**Target:** REST /wp/v2/media/<id>/finalize and wp-admin media-upload.php legacy popup
**Root cause:** finalize_item() stores attacker-controlled sub_sizes[].file verbatim into attachment metadata without sanitize_file_name(); image_get_intermediate_size() concatenates it into a URL that get_media_item() echoes into a single-quoted src attribute with no esc_url/esc_attr.
**Steps:**
1. Author obtains wp_rest nonce from own dashboard
2. POST real JPEG to `/?rest_route=/wp/v2/media` with cookie + `X-WP-Nonce` → 201, id
3. POST `{"sub_sizes":[{"image_size":"thumbnail","file":"a.jpg' /><svg onload='document.title=\"XSS-EXECUTED-\"+document.domain'></svg><b x='",...}]}` to `/?rest_route=/wp/v2/media/<id>/finalize` → 200
4. Administrator GETs `/wp-admin/media-upload.php?tab=library`; payload breaks out of src and executes
**How found:** Source review of new REST finalize_item() write primitive; noticed sub_sizes[].file schema only type:string with no sanitize callback, traced propagation through image_get_intermediate_size() to unescaped sink in get_media_item(), then verified which admin screens render raw vs escaped.
**Exploitation:** Author uploads own attachment, poisons thumbnail sub-size filename with attribute-breakout SVG payload via finalize endpoint; any admin opening the nonce-less legacy media popup executes script in wp-admin origin, enabling nonce theft, admin creation, plugin install.
**Escalation:** Single poisoned attachment renders for every attachment in library, hitting all admins; nonce-less GET deliverable via lure link or iframe.
**Insight:** New REST write endpoints that persist metadata fields (sub_sizes[].file, original_image) often skip sanitize_file_name() that legacy upload paths enforce; trace those fields into legacy admin renderers like get_media_item() that predate esc_url hardening.
