Why this research?

I stumbled upon this tweet by ixSly about a vulnerability in a code snippet found in Outline

You can try to figure out the vulnerability here by yourself:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/// userUrl is user input
/// CSP: "script-src gist.github.com"
const userUrl = new URL(userUrl);

if (userUrl.host === "gist.github.com" && userUrl.protocol === "https:") {
  const gistId = userUrl.pathname.split("/")[2];
  const embedScriptUrl = `https://gist.github.com/${gistId}.js`;

  ctx.body = `
  <html>
    <body>
      <script type="text/javascript" src="${embedScriptUrl}"></script>
    </body>
  </html>
  `;
}

Github Gist JSONP endpoint

I thought this is some bypass to “escape” the src quote of script. Found out the hard way that it is in fact not the way to solve due to CSP. Inputting the CSP into Google CSP Evaluator Google’s CSP Evaluator output shows that there is no bypass.

The writeup mentions using another site https://cspbypass.com to check for possible CSP bypasses. Sure enough, there is two endpoints we can take advantage of.

I cannot find any documentations from Github that mentions this secret callback endpoint. The cspbypass page provides an example payload, which got me wondering whether it is “needed” to use that specific JSON file

1
<script src="https://gist.github.com/renniepak/e7afcd7e727e1a0c481d955ba10441a9.json?callback=alert"></script>

After spending some time trying to figure out the structure of the JSON file, I did not find anything interesting. Desparate, I tried to host my own gist with gibberish content (the .json extension turns out to be not really related):

Github Gist

I can access my Gist using a link like this https://gist.github.com/IceWizard4902/447312912b1ff7fc88abfd66067e16e0. If I append .json extension to the link, e.g https://gist.github.com/IceWizard4902/447312912b1ff7fc88abfd66067e16e0.json, then the resultant output is a bit different, but nothing interesting.

However, when I try adding the callback parameter to the link, e.g https://gist.github.com/IceWizard4902/447312912b1ff7fc88abfd66067e16e0.json?callback=alert, then suddenly the output returned resembles a JSONP endpoint. Even the content type is set to be application/javascript

JSONP output

So we don’t need the specific JSON file from renniepak to have a JSONP endpoint. Rather, the .json append trick + callback parameter will work on any gist.

However, from my quick testing, this JSONP endpoint seems to be quite restrictive. We cannot have opening and closing brackets in the callback parameter. We can specify any function call though, but the 1st parameter is limited to the JSON structure from Github. There might be some ways to bypass this limitation though.

How to solve the challenge

Here’s the link to the solution of the author. They use Shazzer to solve this challenge alongside the Github JSONP endpoint above. Their payload is the following:

1
https://gist.github.com/x/ixSly&sol;98210ba6e8683bd772e857128cd3cdca.json&quest;callback=prompt&amp;x=ixSly

Looking at this, I have some questions.

  • Why are the “special” URL characters encoded as HTML entities? This is to make the new URL() call treat the content following ixSly as the second pathname.

  • Can I use other HTML encoded characters, i.e HTML encoded hex? This unfortunately will not work. Say if we want to encode ? as &#x2f; or any other HTML encoded string with #, the content following the # will be treated as the hash of the URL. Hence, the userUrl.pathname.split("/") will skip the hash portion of the URL.

  • Can we make this payload shorter? I think so (?) Here is my own payload

1
https://gist.github.com/IceWizard4902/447312912b1ff7fc88abfd66067e16e0.json&quest;callback=alert&

which essentially does the same trick. The &amp is a bit redundant, because the & is still treated a part of the pathname when ? is missing from the URL. Probably there is a reason why the author has to encode /, ? and & character as HTML entity.