XSS attacks in Connect apps that serve user-generated HTML/SVG files

A security researcher has found a way to run XSS attacks against several of our apps. I want to share with you the details, as I think many apps might be affected, and the vulnerability might not be immediately obvious.

Summary of the attack

If an app provides a way for users to upload HTML or SVG files and has any API endpoint that will serve these files under the same origin from which it also serves its Connect iframes, an attacker can lure Confluence users to a URL that serves such a HTML/SVG with an embedded malicious JavaScript, enabling the attacker to impersonate the Confluence user who opened the URL on any Confluence instance where the app is installed.

Description of the attack

Let’s say I serve an Atlassian Connect app on https://app.example.com/. In my app, certain users have the possibility to upload HTML or SVG files, for example for templates or avatars for something. Regardless of whether I store these files as Confluence attachments, in my own storage or I am directly proxying them from a public URL, I provide an API endpoint https://app.example.com/api/asset/{filename} where I serve these files.

A user may upload a malicious HTML or SVG file containing JavaScript. Even if my API endpoint to access the uploaded file is normally not exposed to the end-user, the malicious user may share a link to https://app.example.com/api/asset/malicious.svg with other users.

Opening that resource will execute the JavaScript that it contains. Specifically, it will execute the JavaScript under the origin of my app. At first glance, this might not seem like a big deal, since I am not setting any cookies and no JWT is present that I can use to do anything malicious. What might not seem obvious though is that from the JavaScript, I can open a popup to Confluence using const popup = window.open('https://example.atlassian.net/wiki/...). If the opened Confluence page contains an iframe that is served by my app (under my origin), I get full access to that iframe through popup.frames[1]. I can for example generate a token using popup.frames[1].AP.context.getToken() and thus impersonate the Confluence user who has opened my malicious file.

Mitigation

To mitigate the issue, the uploaded files need to be executed under a different origin. This can be achieved in two ways:

  • Either serve the files from a different domain than my Connect iframes
  • Or set the Content-Security-Policy: sandbox header. In particular, the permissions that must not be allowed seem to be allow-popups-to-escape-sandbox and allow-same-origin. For most use cases, it probably works to set the sandbox header without any special permissions.

Notes on JWT-protected endpoints

In many cases, my /api/assets/{filename} endpoint might require a valid JWT. This does not prevent the attack, it just makes it slightly more complicated to perform. Since my app is served from the same domain for every Confluence instance, an attacker simply needs to create any Confluence free instance, install my app there and can then generate valid JWTs there (for example by calling AP.context.getToken() inside the app iframe or by extracting it from the iframe URL). The JWT does not need to belong to the same Confluence instance as the one that is being attacked.

Since JWTs have an expiration time (the one from the iframe URL is valid for 3 minutes, one generated by AP.context.getToken() is valid for 15 minutes). To get around this, an attacker could automate the JWT generation (for example by opening Confluence through a script and extracting the JWT from the iframe URL), set up a page that redirects to my API endpoint with a valid JWT, and use that page for the attack instead.

Special case: Confluence iframe macro

A special way to perform the attack would be to embed my API endpoint into a Confluence iframe macro. This macro makes it possible to embed any web page into a Confluence page as an iframe. This makes it easier to lure a user onto the page, and it makes it possible to access the Connect iframe directly through parent.frames, without opening a popup window. It does require the attacker to be a user on the attacked Confluence instance though.

16 Likes

Thank you for the write-up.

I am wondering if setting Content-Disposition: attachment; would be enough to prevent an attack, since the browser will download the file instead of reading the HTML.

1 Like

Content-Disposition: attachment is a working option. We have used it in production for several years. Here you can find an old discussion about a solution based on it. However, sanitization (for instance, using DOMPurify) might provide a better UX with the same safety level.