This Cross-Site Scripting challenge was very minimal and required finding some weird behaviour from the browser that bypasses the filter put in place to prevent javascript: URLs. Our task:

Inject an alert("Wizer")

Source Code

This challenge's source code was quite simple:

JavaScript

import { useRouter } from 'next/router';

const sanitizeLink = (directLink) => {
  // prevent XSS (replace case insensitive 'javascript' recursively in the URL)
  let searchMask = "javascript";
  let regEx = new RegExp(searchMask, "ig");
  
  while(directLink !== String(directLink).replace(regEx, '')) {
    directLink = String(directLink).replace(regEx, '');
  }
  return directLink;
}

export default function Home() {
  const router = useRouter();
  let { directLink } = router.query;
  const isDirectLink = typeof directLink === 'string' && directLink.length > 0; 

  React.useEffect(() => {
    if (router.isReady && isDirectLink) {
      directLink = sanitizeLink(directLink);
      router.push(directLink);
    }
  }, [router.isReady, isDirectLink]);

  return (
      <main className="text-center mt-5">
        <h3 className="h3 mb-3 fw-normal">Where do you want to go next?</h3>
        <h5 className="h5 mb-2 fw-normal" style={{cursor: 'pointer'}} 
            onClick={() => { router.push("/users")}}>List of Users</h5>
        <h5 className="h5 mb-2 fw-normal" style={{cursor: 'pointer'}} 
            onClick={() => { router.push("/groups")}}>List of Groups</h5>
        <h5 className="h5 mb-2 fw-normal" style={{cursor: 'pointer'}} 
            onClick={() => { router.push("/profile")}}>Your Profile</h5>
        <div className={styles.footer}>
          Powered by <Image src="/wizer.svg" alt="Wizer" width={200} height={100} className={styles.logo} />
        </div>
      </main>
  )
}

It is a NextJS application that takes a directLink parameter from the query string if it exists and redirects the user to this URL after the page has loaded (client-side redirect).

https://event2-1-756y.vercel.app/?directLink=https://example.com -> https://example.com

One common attack for such a pattern is using the special javascript: protocol instead of http:// or https://. This protocol executes JavaScript from part of the URL on the current page instead! Try running location = "java\x09script:alert(origin)" in your browser's DevTools to see that it really executes.

Because the developer knew about this attack, they implemented a sanitization in sanitizeLink() that recursively removes "javascript" from the string until it is no longer present. Another trick that sometimes bypasses such a filter is messing with the capitalization of the protocol (such as jAvAsCrIpT:), but this is prevented here by using a case-insensitive Regular Expression as seen by the "i" flag.

Bypassing the filter

This sanitization function still only looks for the string "javascript", but browsers are actually a bit more lax. The following cheatsheet shows what kind of special characters are allowed in what places of a common XSS payload. There are some very interesting ones, but we are mostly concerned with hiding the javascript: protocol:

It says we should be able to insert \x09, \x0a and \x0d characters in this protocol string, which correspond to tab, new line and carriage return characters respectively. We could have come to the same conclusion ourselves by writing a simple fuzzer that goes through all characters to see if their protocol remains javascript: after inserting them:

https://shazzer.co.uk/vectors/661c86b74c29b7eb4ea1d340

Armed with this knowledge, we can bypass the sanitization filter. All we need to do is insert one of these special characters which the browser seems to ignore, and the regex won't find "javascript" anywhere, while the browser executes our JavaScript payload:

https://event2-1-756y.vercel.app/?directLink=java%09script:alert(%27Wizer%27) -> alert('Wizer')

The above payload solves the challenge!

Mitigation

First of all, it is not clear why the directLink parameter is even part of the application, as it does not show its use anywhere in the source code. Choosing this instead of a direct link to the given page is also questionable, so it should be strongly considered if this parameter is even needed.

If it is required for any good reason, inputs should be sanitized as much as possible. If the ?directLink= parameter is never an external link in real use-cases, it should not be possible to make one. With a Regular Expression like the following, we verify that the URL is only a relative or absolute path to the current page, and cannot redirect to another domain or even another protocol:

Regex

^\/?[\w-]+[\/\w-]*$

Here are some examples:

 

# Will full-match:
path
some/relative/path
/absolute/path

# Won't full-match:
//example.com
javascript:anything