I think, unlike how others have suggested this does not mean prevention against HTML type XSS injection. I will demonstrate by generalised example from an XSS bug bounty I got paid out from a top 1000 website.
Let's say you have a JSONP API (which, from the get-go is a terrible idea).
A Weather API
This JSONP API returns the weather, across origin without using CORS that we query against with the place we want the weather for. JSONP means that you load it as a script, like this:
* http://mywebsite.com/use-weather-api.php *
<script>
function weatherCallback(data) { alert(data.weather) }
</script>
<script src='http://example.com/weather-api?place=$_GET["place"]&callback=weatherCallback'></script>
The contents of this script will be generated based on the inputs, using the callback to pass the response to the parent context, for example if $_GET["place"]
is "london"
:
weatherCallback({"weather": "rainy"})
The function weatherCallback is then called with the JSON payload from the API, saying the weather is rainy.
Counterpoint: HTML Injection
Let's assume we do no sanitisation or contextual sanitisation on our inputs. Now, the simplest and most obvious XSS we can do is send a place that uses HTML injection to get XSS.
* http://mywebsite.com/use-weather-api.php?place='><script>alert('xss!')</script> *
<script>
function weatherCallback(data) { alert(data.weather) }
</script>
<script src='http://example.com/weather-api?place='><script>alert('xss!')</script>&callback=weatherCallback'></script>
See here that by naïvely adding the parameter into our HTML, on the last line we allow the attacker to add extra HTML that generates the extra script tags which, in turn allow them to add extra code to control our webpage, aka XSS. This is an extended version of what others have provided as an example above.
URL Encoding Doesn't Fix This
However, in this example case we should solve this not by using URL Encoding, but by using HTML encoding. If we replace $_GET["place"]
with urlencode($_GET["place"])
then some characters may not be escaped, most notably '
(because it's not a URL entity), meaning we can still get XSS:
* http://mywebsite.com/use-weather-api.php?place='onload='alert(1) *
<script>
function weatherCallback(data) { alert(data.weather) }
</script>
<script src='http://example.com/weather-api?place='onload='alert(1)'callback=weatherCallback'></script>
In this case, we reach XSS by injecting a separate parameter to our <script>
tag that sets a malicious onLoad
event. We can do this because key HTML special characters are not escaped by URL encoding.
Query Parameter Injection
Let's assume we have perfect HTML sanitisation now. The guarantee the HTML sanitiser will give us is that it prevents things we add escaping the HTML entities they're placed in, causing our previous attacks to fail. However we have one last trick up our sleeve, injecting into the URL context:
* http://mywebsite.com/use-weather-api.php?place=london%26callback=alert# *
<script>
function weatherCallback(data) { alert(data.weather) }
</script>
<script src='http://example.com/weather-api?place=london&callback=alert#callback=weatherCallback'></script>
In this case, in spite of our perfect HTML sanitisation and due to the lack of contextual URL sanitisation, we've allowed extra query parameters to be injected into our request URI. Note that the &
is encoded to &
, but will be decoded to &
by the time it reaches the browser's URL parser.
Because the 'correct' callback parameter has been 'edited out' with #
, and our malicious one injected, the generated script will be:
alert({"weather": "rainy"})
Allowing us to call any function as the webpage (i.e. XSS). We can go further and, in many cases write any javascript we want here, not just function calls, e.g.:
alert('hello!');({"weather": "rainy"})
Where our query parameters are place=london&callback=alert('hello!')#callback=weatherCallback
.
This is a case where it is important to urlencode and then HTML encode.
So, what can I do?
Don't do this manually. It's not the MySpace days anymore. Use a templating system that provides contextual sanitization. This means it understands where you're putting your inputs in the HTML, and for the most part automatically uses the right sequence of escapes to ensure that malicious code injection doesn't happen -- today we talked about URL syntax in HTML syntax, but sometimes you have a URL inside a <script>
tag inside HTML, in which case you need URL escaping, then javascript escaping, then HTML escaping. It's maddening!
Most good templating systems do it these days. PHP's default one doesn't, but there are other options you can use. Do make sure the option you pick is XSS safe.