Help us understand the problem. What is going on with this article?

Yii2で暗号化クッキーとプレーンなクッキーを併用する

More than 5 years have passed since last update.

4連投のつもりが5連投目です。

Yii 2 は標準で Cookie を暗号化してバリデーションします。ユーザーが Cookie の内容改ざんを行えなくなるのでセキュアで結構...なのですが...

RequestenableCookieValidation が効いていると、無条件にすべての Cookie が暗号化されます。つまり、いったん暗号化を選んだら、暗号化しない Cookie を送る方法がなくなってしまいます。そうすると、リバースプロキシやロードバランサーですら、アプリケーションで作った Cookie にはいっさいアクセスできなくなってしまいます。分散/キャッシュの仕組みによっては、アプリケーション内で決定される何かの値が、内部的な経路を決めるヒントになる場合もあるでしょう。

そこで、そんな特別な状況では、「例外的にこれは暗号化しない」という指定が可能な Request/Response を作ってそれを使います。

<?php
namespace app\utils\web;

use Yii;
use yii\base\InvalidConfigException;
use yii\web\Cookie;

/**
 * @inheritdoc
 */
class Request extends \yii\web\Request
{
    /**
     * Cookie names which ignores encryption and validation.
     * @var array
     */
    public $insecureCookies;

    /**
     * @inheritdoc
     */
    protected function loadCookies()
    {
        if ($this->enableCookieValidation) {
            if ($this->cookieValidationKey == '') {
                throw new InvalidConfigException(get_class($this) . '::cookieValidationKey must be configured with a secret key.');
            }
        }

        $cookies = [];
        foreach ($_COOKIE as $name => $value) {
            if ($this->enableCookieValidation && !in_array($name, $this->insecureCookies)) {
                if (is_string($value) && ($value = Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey)) !== false) {
                    $cookies[$name] = new Cookie([
                        'name' => $name,
                        'value' => @unserialize($value),
                        'expire'=> null
                    ]);
                }
            } else {
                $cookies[$name] = new Cookie([
                    'name' => $name,
                    'value' => $value,
                    'expire'=> null
                ]);
            }
        }

        return $cookies;
    }
}
<?php
namespace app\utils\web;

use Yii;

/**
 * @inheritdoc
 */
class Response extends \yii\web\Response
{
    protected function sendCookies()
    {
        $request = Yii::$app->getRequest();
        if (!$request->enableCookieValidation) {
            parent::sendCookies();
            return;
        }

        $insecureCookieNames = $request->get('insecureCookies', []);

        // separate cookies if secure or insecure
        $secureCookies = [];
        $insecureCookies = [];
        foreach ($this->getCookies() as $cookie) {
            if (in_array($cookie->name, $insecureCookieNames)) {
                $insecureCookies[] = $cookie;
            } else {
                $secureCookies[] = $cookie;
            }
        }
        $this->getCookies()->removeAll();

        // send secure cookies
        foreach ($secureCookies as $cookie) {
            $this->getCookies()->add($cookie);
        }
        parent::sendCookies();

        // send insecure cookies
        foreach ($insecureCookies as $cookie) {
            setcookie($cookie->name, $cookie->value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly);
        }
    }
}

使い方

<?php
return [
    // ...
    'components' => [
        'request' => [
            'class' => 'app\utils\web\Request',
            'enableCookieValidation' => true,
            'cookieValidationKey' => 'xxxx',
            'insecureCookies' => [
                'insecure_cookie_name',
                'another_insecure_cookie_name',
            ],
        ],
        'response' => [
            'class' => 'app\utils\web\Response',
        ],
    ]
];

これで、 insecure_cookie_name および another_insecure_cookie_name と名付けられた Cookie はセキュリティの保護を受けない代わりに、経路上で読み取ったり、JavaScript でアクセスしたりできるようになります。

(Tips のつもりが案外長いコードになってしまいました)

ただし、こんな平文 Cookie は、JavaScript 連携用のワンタイムトークンや、ロードバランサーの振り分け根拠など、単一の値で表されるものだけにしましょう。Cookie が構造的になると、壊れた文字列を送ってパースエラーを起こさせる攻撃が可能になってしまいますから。もちろん、ユーザーにとって致命的でなく、かつ揮発性の高いデータであるという条件は言うまでもありません。

また、いったん使い始めたら、暗号化していたものを暗号化しない方針に途中で変更することはできません。暗号化した Cookie を持っているブラウザが送ってきた値を、暗号化していない Cookie を想定して受け取ると、アプリケーション内では「解読できないそういう文字列」になってしまいます。十分に注意してください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした