Skip to content

Instantly share code, notes, and snippets.

@12joan
Last active October 1, 2024 11:24
Show Gist options
  • Save 12joan/1548c6ffa99186f17074e52b48fb9ae3 to your computer and use it in GitHub Desktop.
Save 12joan/1548c6ffa99186f17074e52b48fb9ae3 to your computer and use it in GitHub Desktop.
A guide to mitigating XSS for link and embed components in Slate

Just a reminder for anyone using Slate:

⚠️ Your app's link and embed components are vulnerable to XSS unless you explicitly check the URL protocol in the component. Attackers can use this to hijack your users' accounts.

// ❌ Vulnerable code
return <iframe src={element.url} />

// ❌ Vulnerable code
return (
  <a href={element.url}>
    {children}
  </a>
)
recording.mp4

This is an incredibly common mistake I see Slate apps making, including Slate's official examples for links and embeds.

Any data you get from the element object should be considered untrusted, as it's trivially easy for attackers to inject arbitrary data here either by tricking victims into opening malicious Slate documents or pasting untrusted Slate fragments.

If you allow untrusted input in the src attribute of an iframe or the href attribute of a hyperlink, attackers can use the javascript:, data: or (in some cases) vbscript: schemes to hijack your users' accounts.

The iframe case is especially nasty as the victim doesn't need to click anything for the malicious code to be executed.

The only safe place to fix this is in the React component where you're rendering the <a> or <iframe> DOM element. Using normalizers or validating the URL when it's first inputted are not sufficient.

Mitigations should parse the URL and explicitly check the protocol against an allowlist. If the URL can't be parsed or the protocol isn't allowed, the URL should be discarded as unsafe.

If you need to support relative URLs, you can pass your base URL to the second argument of the new URL() constructor.

const allowedSchemes = ['http:', 'https:', 'mailto:', 'tel:']

// ...

  const safeUrl = useMemo(() => {
    let parsedUrl: URL = null
    try {
      parsedUrl = new URL(element.url)
      // eslint-disable-next-line no-empty
    } catch {}
    if (parsedUrl && allowedSchemes.includes(parsedUrl.protocol)) {
      return parsedUrl.href
    }
    return 'about:blank'
  }, [element.url])

GitHub's code scanning feature (free for public repositories) can help you locate issues like this in your code and, in some cases, evaluate the effectiveness of your mitigation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment