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...

Security Tip: Safely Rendering JSON in Blade

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!