Smart Communication Design Company
ホーム > ナレッジ > Blog > フロントエンドBlog > 2019年12月 > webhintのヒントを自作する

webhintのヒントを自作する


UI開発者 加藤

この記事はミツエーリンクス Advent Calendar 2019 - Qiitaの1日目の記事です。

ミツエーリンクスは今年初めてアドベントカレンダーに参加します!フロントエンドだけでなく、アクセシビリティやWebサイトの品質に関する記事もアップされる予定なので、是非チェックしてみてください!

先日webhintを活用しようという記事でwebhintの使い方を説明しましたが、今回は「ドメインをまたぐリクエストの場合、CookieにSameSite属性とSecure属性がついているかチェックする」をテーマにwebhintのヒントを自作してみたいと思います。

2020年2月にリリース予定のGoogle Chrome 80からCross-siteなリソースに関連付けられたCookieは明示的にSameSite=None;Secure属性が付与されていないと送信されなくなります。FirefoxやEdgeでも同様に実装されるようなのでCookieに必要な属性が付与されているかどうかをwebhintにチェックしてもらいましょう!

※「新しい Cookie 設定 SameSite=None; Secure の準備を始めましょう」も参照ください。

新しいヒントの初期化

新しくヒントを自作する際はcreate-hintというイニシャライザーを利用できます。適当なフォルダで

npm init hint

を実行するといくつかの質問が繰り返され、回答をもとにベースとなるファイルが作成されます。選択したカテゴリによってチェックを行うタイミングが変わったりしますが、あとで追加・変更することも可能なのであまり意識しなくても大丈夫です。今回は以下のように設定しました。

質問 回答
ヒントの名前 cross-site-cookie
ヒントの説明 Cross-siteなCookieにSameSite=NoneSecure属性が指定されているかをチェックします
カテゴリ security
ユースケース Resource Request
ヒントのスコープ site

4つ目のユースケースには以下の4つの選択肢があります。

イニシャライザーの処理が終わると、いくつかのファイルとフォルダが作成されています。まずは必要なパッケージをインストールするために

npm run init

を実行しましょう。本記事を公開した時点ではhint@types/requestがないことでエラーになってしまったため、別途インストールしました。

ヒントクラスの基本

ヒントのチェックロジックはsrc/hint.tsに書いていきます。まずはこのhint.tsの中身を基本的な箇所だけ抜粋して見てみます。

import { HintContext } from 'hint/dist/src/lib/hint-context';
import { IHint, FetchEnd } from 'hint/dist/src/lib/types';

export default class CrossSiteCookieHint implements IHint {

    public constructor(context: HintContext) {
        const validateFetchEnd = (fetchEnd: FetchEnd) => {
            const { resource } = fetchEnd;

            if (Math.ceil(Math.random()) === 0) {
                context.report(resource, 'Add error message here.');
            }
        };

        context.on('fetch::end::*', validateFetchEnd);
    }
}

新しく作るヒントクラスはIHintというインターフェースを実装する必要があります。ポイントとなるのは

context.on('fetch::end::*', validateFetchEnd);

の部分です。jsdomPuppeteerなどのコネクタはこのイベントを通して、各ヒントクラスに情報を渡します。ここで指定されているfetch::end::*というのは、サイトを開いたあと各リソースの読み込みが完了するたびに発火するイベントです。イベントの詳細はEventsページで参照できます。

また、

context.report(resource, 'Add error message here.');

の一文が実行されると、このヒントには通らなかったこととなり最終的に出力される結果レポートでエラーとして報告されます。一度if文の外にこの一文を出して毎回エラーになるかを試してみましょう。まずは、TypeScriptファイルをビルドします。

npm run build

ビルドが成功するとdistフォルダが作成されその中にJavaScriptファイルが出力されているはずです。つづいて、今回新規で作成した「cross-site-cookie」によるチェックが走るように.hintrcを調整します。

{
    "connector": {
        "name": "jsdom",
        "options": {
            "waitFor": 1000
        }
    },
    "formatters": ["html"],
    "hints": {
        "cross-site-cookie": "error"
    },
    "hintsTimeout": 120000
}

ここまでできたらあとはwebhintを実行するだけです。

npx hint https://www.example.com

結果、以下のキャプチャのようなHTMLが吐き出されました。

吐き出されたレポート

新しく作成した「cross-site-cookie」というヒントが「SECURITY」カテゴリに分類され、すべてのリクエストがエラーとして報告されているのが分かります!

チェックのロジックを作る

さて、べースはでき上がったので実際にチェックするロジックを書いていきましょう。

fetch::end::*イベントのコールバック関数(validateFetchEnd)にはFetchEndオブジェクトが引数として渡されますが、このオブジェクトはresourcerequestresponseプロパティを持っており、その中のresponseからCookieの情報を抜き取ります。Cookieのパース処理はCookieを利用しました。チェックの流れは以下の通りです。

シンプルに実装すると、

const validateFetchEnd = async (fetchEnd: FetchEnd) => {
    const { resource, response } = fetchEnd;
    const cookieList: string | string[] = response.headers && response.headers['set-cookie'] || '';

    if (!cookieList) {
        return;
    }

    for (let i = 0, len = cookieList.length; i < len; i++) {
        const cookie = cookieParser.parse(cookieList[i]);
        const domain = cookie.Domain || cookie.domain;

        // ドメインが同じならチェックしない
        if (domain && domain === {SITE_DOMAIN}) {
            return;
        }

        // Cross-siteでかつ、SameSite属性がない場合もしくは「none」に設定されていない
        if (!cookie.SameSite || cookie.SameSite !== 'None') {
            context.report(resource, `A cookie associated with a cross-site resource at 「${domain}」 was set without the 'SameSite' attribute`);

            return;
        }

        // Cross-siteでかつ、Secure属性が指定されていない
        if (!cookie.Secure) {
            context.report(resource, `A cookie associated with a resource at 「${domain}」 was set with 'SameSite=None' but without 'Secure'.`);

            return;
        }
    }
};

のようになります。TypeScriptをビルドし直してから、特定のサイトで実行してみます。

Cross-siteなCookieのうち必要な属性が付与されていないものが警告されてますね!これで新しいヒントの作成はおしまいです。webhintの公式ページでは「ページにコピーライトがあるかどうかをチェックするヒントのサンプル」が紹介されていますので、興味がある方は参考にしてみてください。