Secure your WYSIWYG editor in Laravel

A WYSIWYG editor enables users to provide styled content familiarly. The result of most WYSIWYG editors is HTML which is stored and displayed on your website or web app.

But… we are told to never trust user input. This is even more true when the user can provide HTML. This could easily result in an XSS attack, broken layouts, or other problems you don’t want.

Fortunately, there are some best practices to help with this, in short:

  • Store the data as you get it
  • Sanitize before displaying the data

Store data as you get it

By not sanitizing the data before storing it, you can change the sanitizing without losing data. For example: you don’t allow the strikethrough element at first. When development continues, the team decides to allow the element. Without sanitizing while storing, you still have the element in the data and can show it by modifying the sanitizer.

Sanitize before displaying the data

Before we show the data on the website or web app, we need to sanitize the data.

Add sanitizer package

Let’s implement this in Laravel by adding a sanitizer package. I like the purify package created by Steve Bauman which can be installed with composer:

composer require stevebauman/purify

And publish the config file in which you can configure which tags are allowed:

php artisan vendor:publish --provider="Stevebauman\Purify\PurifyServiceProvider"

In the config file there’s a HTML.Allowed property that contains a list with allowed tags. It contains a sane default, but feel free to customize it to your own needs.

Sanitize the field

Laravel has a nice feature called Attribute Casting. This provides the ability to use similar accessors and mutators for multiple models with any additional methods. We’re going to create a custom cast that will perform the sanitizing.

Start by creating a custom cast:

php artisan make:cast SanitizedHtml

Now open the cast and add the sanitize functionality when retrieving the data:

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Stevebauman\Purify\Facades\Purify;

class SanitizedHtml implements CastsAttributes
{
  	/**
     * Cast the given value.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  mixed  $value
     * @param  array  $attributes
     * @return mixed
     */
    public function get($model, string $key, $value, array $attributes): string
    {
        return Purify::clean($value);
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  mixed  $value
     * @param  array  $attributes
     * @return mixed
     */
    public function set($model, string $key, $value, array $attributes): string
    {
        return $value;
    }
}

The latest step is to apply the casts to a field of the model:

protected $casts = [
    'content' => SanitizedHtml::class,
];

When the content property of the model is displayed, it will use the sanitized content. Make sure to use the proper field of your model.

Access to unsanitized data

While the Eloquent ORM will return the sanitized data by default, it does offer the ability to access the not sanitized value too. In the case you need to access the value without being sanitized, use: $model->getRawOriginal('content');

Note about VueJS

Are you using VueJS in your application? The user might be able to provide input like {{ item }}.

When these data are outputted by blade, it might be the case VueJS is rendering these (which isn’t a problem when using v-html). So make sure to escape the curly braces when necessary.