【これで解決】SPAでセッション管理するならCookieにしよう【CakePHP4編】

ここ最近の主流はSPA(シングルページアプリケーション) + APIサーバの構成になってます。

フロントとサーバーサイドを分断し、疎結合とするアーキテクチャですね。

疎結合とするメリットとしてはエラー発生時の原因の特定のしやすさですかね。
サーバ側はAPIサーバとして存在しているためフロント側は特定する必要がないわけですし、不特定多数存在しても良いわけです。

フロント側もVue.js、React、純粋なHTML+Javascriptなど、好きなフレームで構成すれば良いです。
フロントとサーバでRESTのお作法さえしっかり抑えておけば他は特に干渉し合う必要もありません。

流行りの開発手法になってきている意味もわかります。

だけど。。。

フロントとサーバサイドが別環境になることでの弊害もあるわけです。

そう、セキュリティですね。

別環境ということは別オリジンであり、別ドメインとなるわけです。

ということでこの辺で抵触してしまいそうなセキュリティリスク、キーワードをおさえておきましょう。

■Origin(オリジン)
原点のこと。
例)https://hoge.com:443
スキーム→https
ホスト→hoge.com
ポート→443

■CSRF(ClossSiteRequestForgeries)
攻撃者がブラウザなどのユーザ・クライアントを騙し、意図しないリクエスト(たとえばHTTPリクエスト)をWebサーバに送信させる。CSRF対策をとっていないサーバだと正規リクエストとして処理をしてしまう。
httpOnly属性をつけたCookieであっても何かしらの攻撃を受け傍受された場合、CSRF対策をしていないサーバは正規リクエストとして扱ってしまう。CSRFトークンの仕組みは大事。

– Secure属性
https通信のときのみCookieを送信する

– HTTPOnly属性
cookie のスコープ(参照・操作の権限)を HTTP リクエストに制限するもの。
クライアント側でJavaScriptによって参照、編集することができなくなる。

■XSS(クロスサイトスクリプティング)
Webアプリケーション内に悪意のあるスクリプトを埋め込み、スクリプトを実行させ個人情報などを抜き取る攻撃手法。
掲示板のようなサイトにスクリプト付きリンクを投稿し第三者がそのリンクを踏んだ際に悪意あるスクリプトが実行される、というような説明をよくみる。
このケースのみでなく気をつけておきたいのは、CDNなどの外部スクリプトを呼び出しているようなWebアプリケーション。
例)以下のような外部スクリプト読み込み。google製を例にとっているが、こういった感じで読み込んでいるスクリプトのどれもが信頼できるとは限らないということ。

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
XSSとCSRFの違いXSSCSRF
脆弱性WebアプリWebアプリ
実行契機不正なURLへのアクセス不正なURLへのアクセス
実行場所ブラウザサーバ
可能な攻撃スクリプトなので何でもサーバ側で定義されている処理
前提なしログイン状態
XSSとCSRFの違い表

■CORS(Cross-Origin Resource Sharing)
追加のHTTPヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組み。
サーバがブラウザに指示するというところがポイント。
ブラウザ側はCORS制約を検知するとエラーの挙動をとる。なのでPostmanのようにhttpリクエストを投げるようなツールはCORS制約を受けない。あくまでCORS制約を受ける場合においてはサーバ側からのレスポンスを読み取らせないというブラウザに備わる機能。

※深堀り!
CORS対策をブラウザがしているのであればCSRFトークンといった対策をサーバ側はする必要ない?と思ってしまうけど、そうじゃない。
先述したようにCORSはあくまでブラウザの挙動。CSRF対策はサーバ側の責務。「CSRF対策をとってないサーバへリクエストした場合、ブラウザ側はCORSエラーとなっていてもサーバ側では正常リクエストとして処理が完結してしまう」という点に注意。

■プリフライト
クロスオリジンのリクエスト先に固有のHTTPヘッダを付与しようとする際に、ブラウザは安全確認のためにサーバ側にプリフライトリクエスト(OPTIONメソッド)を予め送信する。
固有のHTTPヘッダとはContentTypeやAuthorizationなど。

JWT管理はローカルストレージ?Cookie?論争

SPAでセッション管理をしようと思うとJWTがまず候補にあがる。
そしてこのJWSの保管先としてはローカルストレージとCookieどちらで持つのがセキュアか、ここには色々と論争がある。

両者の違いは
・ローカルストレージはJavaScriptから操作(生成、参照、編集、削除)が可能
・Cookieはサーバ側からセットされる。httpOnly属性がついていないCookieはJavaScriptで操作可能

ローカルストレージはスクリプトからいくらでも参照、改ざんを可能としてしまうためXSS攻撃を受けてしまうといくらでも不正に扱われてしまう。
JWTは簡単にデコードできてしまうけど署名をつけているため改ざんは検知できる。
なのでサーバ側で改ざんを検知すれば不正リクエストとして扱うことは可能。ここでの問題はJWTがXSSを通じて第三者に盗まれてしまうこと。JWTの有効期限内であれば攻撃者もログイン状態を成立させれてしまうことにある。

Cookieもローカルストレージと同様に扱われる。しかしhttpOnly属性をつけることでローカルストレージよりは強固にできる。スクリプトからの操作ができないためXSS攻撃を受けてもJWTは傍受されない。
ここでの問題はこのhttpOnlyのsetCookieヘッダもハッカーなどの手にかかれば作り出せてしまうこと。本来httpOnlyで守られているはずのJWTも他人に使われる前提でやはりサーバ側も対策しておかなければならない。
それがCSRF対策。サーバ側から発行される部外者では生成不可能なトークンを渡しておくことで、以降のリクエストはCookieによるJWTとCSRFトークンをチェックすることで正常リクエストの判断をおこなうという対策。

ここから導き出される結論は以下とした
・JWTはhttpOnly属性を付与したCookieで管理→XSS対策
・JWT + CSRFトークンを使う→CSRF対策

いざ実装

SPAフロント側のaxiosの例

const setCookie = () => {
    axios
      .get('https://hoge.com/api/cookie/set', {
        withCredentials: true,
      })
      .then(function (response) {
        console.log(response);
      })
      .catch(function (error) {
        console.log(error);
      });
};

リクエストにCookieを添えて送信する場合はwithCredentials: trueを明示的にセットしておく必要がある。

サーバ側の一例(CakePHP4系)

Application.php

if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
    // プリフライト用のヘッダ
    header('Access-Control-Allow-Origin: https://hoge.com');
    header('Access-Control-Allow-Credentials: true');
    header('Access-Control-Allow-Methods: POST, PUT, PATCH, DELETE, OPTIONS');
    header('Access-Control-Allow-Headers: authorization, Content-Type');
    exit(0);
} else {
    // 正規リクエスト用のヘッダ
    header('Access-Control-Allow-Origin: https://hoge.com');
    header('Access-Control-Allow-Credentials: true');
    header('Access-Control-Allow-Methods: POST, PUT, PATCH, DELETE, OPTIONS');
    header('Access-Control-Allow-Headers: authorization, Content-Type');
}

// CORS設定
$csrf = new CsrfProtectionMiddleware([
    'httponly' => true,
    'secure' => true,
    'samesite' => 'None'
]);

$csrf->skipCheckCallback(function($request) {
    // 一部のController,actionの場合はcorsを無視する
    $controller = $request->getParam('controller');
    $action = $request->getParam('action');
    if (strcmp($controller,'Api') == 0 && 
        strcmp($action,'setHttpCookie') == 0) {
        return true;
    } else {
        return false;
    }
});

CSRFにoptionをつける場合は
'httponly’ => true,
'secure’ => true,
'samesite’ => 'None’
で付与が可能。
これをしておくとsetCookieによりCookieヘッダにcsrfトークンが付与される。
これらはその際にアタッチされる属性。
※Cakephpのcsrfトークンの特徴なのか、レスポンスで受け取ったcsrfトークンは次のPOST時にセットされていても無効なトークンとして処理されない。
おそらくは1度しか使用できないワンタイム性でトークンが発行されていると思われる。
なのでサーバ側をCakephpとした場合はcsrfトークンはオリジナルで発行した方が良いと思われる。

JWTをsetCookieする部分(Controller)

// JWTをSecure属性、httpOnly、SameSite=NoneでCookieセット
public function setHttpCookie()
{
    // ビルダーメソッドを使用
    $cookie = (new Cookie(self::SESSION_KEY))
        ->withValue('this is JWT')
        ->withExpiry(new \DateTime('+1 hour'))
        ->withPath('/; SameSite=None')
//      ->withDomain()
        ->withSecure(true)
        ->withHttpOnly(true);

    return $this->response
        ->withCookie($cookie)
        ->withStatus(200);
}

httpOnly、Secure、SameSiteをCookieにセットする場合のセットのやり方。
Chromeの新しい仕様としてはデフォルトでSameSiteがLaxとなる。
Laxはクロスドメインを許可しないためここでは明示的にSameSite=Noneをセット。
SameSite=Noneの場合はSeruce属性が必須となる。
ブラウザの制約も回避した上でセキュアにCookieをセットするとなれば、フロント側もサーバ側もどちらもhttpsは必須となることをおさえておきたい。

フロントからセットされたCookieの確認(Controller)

public function verificationCookie()
{
    $userAgent = $this->request->getHeaderLine('User-Agent');
    $session_key = $this->request->getCookie(self::SESSION_KEY);
    $csrf_token_key = $this->request->getCookie(self::CSRF_TOKEN_KEY);
}

一応参考としてアクセス元のユーザーエージェントとCookieの中身の確認方法を記載しておく。
先述のとおりフレームワークによってレスポンスされたCORSトークンはここでリクエストされても無効になってしまう。

まとめ

JWT界隈でここ最近検証したことを反復と備忘のためにまとめてみた。
サーバサイドがCakephp以外になるとまたトークンの使い方などは変わるため、その辺もいずれ検証したいと思う。

ただ、脆弱性の保管はどんなに取り繕っても100%完全なものは築きようがない。

最大のセキュリティホールは人であり、利用者のモラルであると思ってる。
ログアウトせずにPCを放置してしまったり、ID・パスワードをオープンな場においてしまえば脆弱性対策は全く機能しなくなる。

技術者として「これだから大丈夫」としっかり理由とロジカルな説明ができる状態をもっておくことが大事。
XSSが危険、と言われたところで「外部リソースについてはすべて静的にもっており、それ以外のスクリプト攻撃はフレームワークのこの機能が担保してくれています」とシステム基盤をしっかり根拠と見える形で説明できれば良いのだと思われる。

“なんとなく"とか、"一般的によく言われるから"とか、そんなお粗末な理由は卒業したい。

さて、今日もゴリゴリ楽しくやっていきましょうか!