Lago Link XSS


Lago is an open-source billing platform focused on solving the pain points in the billing market. They are backed by some of the top investors in the space including Y Combinator, SignalFire, and Firstmark. The part of Lago I found interesting was that they are open source while being VC-funded. From what I’ve seen, that is not a common practice, but I think it is a positive difference.

Every week or so, I go on GitHub’s trending page for different languages to find vulnerabilities in commonly used products. In February, Lago was trending on GitHub, so I decided to test their repository and product.

Sweet and Simple

The first test that I do for any repository I’m looking at is to search for the phrase innerHTML or whatever keyword is used to signify that in the framework used. For React, that is both innerHTML and dangerouslySetInnerHTML. Through the GitHub search I found one interesting result.

// If there's no internal link, simply return the sanitized string
if (!internalLinks.length) return <span dangerouslySetInnerHTML={sanitized} />

sanitizedWithInternalLinks.push(
    <span key={i} dangerouslySetInnerHTML={{ __html: string }} />,
)

While the first clause looked normal and used a standard sanitizing library, the second clause did not.

Internal Use Only

What I gathered from the code after a short review was that there were two ways the link could be rendered—as either an external or internal link. From there, I was slightly confused about what made an item count as an internalLink since I had not learned TSX/React and was unsure of the control flow. With the help of AI, I was able to parse the file and find the lines that determined internality:

if (!!attribs['data-text']) {
  internalLinks.push(attribs)

  return {
    text: '{{link}}', // This will be replaced later by the <Link /> component
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
  } as any

In retrospect, it is a little sad that I had to use AI to find this. However, I didn’t know at the time whether it was a vulnerability, so I used AI instead of analyzing every issue by hand.

Visitor Pass

As you can see from the code, the check for whether an element is an internalLink is simply whether it has the data-text attribute. After discovering this, I realized that if I could inject any text into the component, I would have an XSS.

RIP Grep

At this point, I did a search for the component, Typography.tsx, and got 100 results. I went through about twenty before deciding it was not worth it to do manually. I asked Codex for the paths where Typography was used and got a couple of results. After going through those results, I found that the delete-customer dialog used Typography for the name of the customer.

Customer Service

Since adding a customer isn’t a high-permission task, anyone able to create a customer could cause an XSS. And, worse, because the sanitizer allowed many formatting tags by default, a bad actor could format the page to make the link appear as the confirm or cancel button.

window.location.href = “javascript:alert(‘XSS’)”

After finding out this issue was exploitable, I attempted to report it to Lago’s team. Unfortunately, they did not have any contact information listed publicly. I went into their Slack as my second option and asked there about how to report the bug. One of the team members reached out to me and pointed me to the correct place to report the vulnerability.

await bug.fixed()

The Lago team was gracious enough to provide me with some swag for finding the vulnerability even though they had already flagged it internally. I got a hat, shirt, pin, and water-bottle sticker, which were all amazing. One funny result of this was the shipping. It shipped from France to the US, where I live---so the cost of shipping ended up being more than the cost of the items themselves.

bug.finally

I’m super thankful to the Lago team for fixing this issue and sending me swag. I learned a lot through this interaction, specifically about how any exception to a sanitizer needs to be checked for leakage, or it can cause a vulnerability.