Security Tip: Safely Rendering JSON in Blade
[Tip#41] It's quite common to inject JSON into Blade templates for various use cases, but is it actually safe to do so? Not really...
It’s a pretty common approach to pass data between your backend and frontend through Blade by rendering a block of JSON into a JS variable.
Something like this:
<script>
var options = <?php echo json_encode($options); ?>;
</script>
However, is this safe?
Well, it depends... In earlier versions of PHP, it wasn’t safe. Now… maybe?
The issue is that json_encode()
isn’t designed for outputting a safe block of JSON within a script block inside HTML. It’s default encoding only handles a basic subset of special cases, and there is always the risk that a new bypass is discovered that will allow for breaking out of the JSON and injecting a custom script.
Some of my favourites from older versions of PHP include simply using a </script>
tag or special “quotes”
that PHP will ignore but Javascript will translate into standard quotes. (I was disappointed I couldn't get a demo working of these!)
So what should you do instead?
Laravel provides a helper class, Illuminate\Support\Js
(source), which includes the extra encoding flags you want when using `json_encode()`
within HTML.
Using it is trivial:
<script>
var options = {{ Js::from($options) }};
</script>
It’s super simple to use - in fact it’s even shorter than the previous code - and will ensure no one can sneak anything into your JSON.
To wrap up, let’s review the difference in output - which nicely shows the benefits in using the helper.
<script>
// json_encode();
var options = {"elves":"three","dwarves":"seven","men":"nine","dark lord":"one"};
// Js::from()
var options = JSON.parse('{\u0022elves\u0022:\u0022three\u0022,\u0022dwarves\u0022:\u0022seven\u0022,\u0022men\u0022:\u0022nine\u0022,\u0022dark lord\u0022:\u0022\\u003C\\\/script\\u003E\u0022}');
</script>
Update: 2023-05-24
A recent addition to Laravel added a new Js::encode()
helper, which you can use to encode JSON without the addition of JSON.parse(...)
that Js::from()
adds.
> Js::encode(['elves' => 'three', 'dwarves' => 'seven']);
= "{"elves":"three","dwarves":"seven"}"
These two helper functions should remove the need to use @js()
and @json()
entirely.
Don’t forget, you can put these safely inside {{ ... }}
tags too!