luyadev/luya

View on GitHub
core/web/filters/JsonCruftFilter.php

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
<?php

namespace luya\web\filters;

use Yii;
use yii\base\ActionFilter;
use yii\web\Response;

/**
 * Json Cruft Filter.
 *
 * This ActionFilter will append the {{luya\web\filters\JsonCruftFilter::$cruft}} string before every request
 * in order to disallow json hijacking.
 *
 * ```php
 * public function behaviors()
 * {
 *     return [
 *         'class' => luya\web\filters\JsonCruftFilter::class,
 *     ];
 * }
 * ```
 *
 * On the client side (example using angular) you have to remove the cruft string from every response content
 * in order to have a valid json response.
 *
 * AngularJs built in protection: https://docs.angularjs.org/api/ng/service/$http#interceptors => JSON Vulnerability Protection
 *
 * @see https://blog.portswigger.net/2016/11/json-hijacking-for-modern-web.html
 * @see https://stackoverflow.com/a/3270390
 * @author Basil Suter <basil@nadar.io>
 * @since 1.0.7
 */
class JsonCruftFilter extends ActionFilter
{
    public const CRUFT_HEADER_NAME = 'X-CRUFT-LENGTH';

    /**
     * @var array A list of status codes which does not need cruft prepend as it has by defintion no content.
     * @since 1.6.2
     */
    public $ignoreStatusCodes = [204];

    /**
     * @var string The curft string which is appended to every json rest response.
     */
    public $cruft = ")]}',\n";

    /**
     * Get the string lengt from the cruf.
     *
     * @return number
     */
    public function getCruftLength()
    {
        return strlen($this->cruft);
    }

    /**
     * Prepend the cruft string to a given content.
     *
     * @param string $content
     * @return string
     */
    public function prependCruft($content)
    {
        return $this->cruft . trim($content);
    }

    /**
     * @inheritdoc
     */
    public function afterAction($action, $result)
    {
        if (Yii::$app->response->format == Response::FORMAT_JSON && !in_array(Yii::$app->response->statusCode, $this->ignoreStatusCodes)) {
            Yii::$app->response->headers->set(self::CRUFT_HEADER_NAME, $this->getCruftLength());
            Yii::$app->response->on(Response::EVENT_AFTER_PREPARE, function ($event) {
                $event->sender->content = $this->prependCruft($event->sender->content);
            });
        }

        return $result;
    }
}