2024年2月13日

クッキー、document.cookie

クッキーは、ブラウザに直接保存される小さなデータ文字列です。それらはHTTPプロトコルの 一部であり、RFC 6265仕様で定義されています。

クッキーは通常、WebサーバーによってレスポンスのSet-Cookie HTTPヘッダーを使用して設定されます。その後、ブラウザはそれをCookie HTTPヘッダーを使用して、同じドメインへの(ほぼ)すべてのリクエストに自動的に追加します。

最も広く普及しているユースケースの1つは、認証です。

  1. サインイン時に、サーバーはレスポンスのSet-Cookie HTTPヘッダーを使用して、一意の「セッション識別子」を含むクッキーを設定します。
  2. 次回、同じドメインにリクエストが送信されると、ブラウザはCookie HTTPヘッダーを使用してクッキーをネットワーク経由で送信します。
  3. そのため、サーバーは誰がリクエストを行ったかを知ることができます。

document.cookieプロパティを使用して、ブラウザからクッキーにアクセスすることもできます。

クッキーとその属性については、多くの難しい点があります。この章では、それらを詳細に説明します。

document.cookieからの読み取り

あなたのブラウザはこのサイトからクッキーを保存していますか?見てみましょう。

// At javascript.info, we use Google Analytics for statistics,
// so there should be some cookies
alert( document.cookie ); // cookie1=value1; cookie2=value2;...

document.cookieの値は、;で区切られたname=valueのペアで構成されています。それぞれが個別のクッキーです。

特定のクッキーを見つけるには、;document.cookieを分割し、正しい名前を見つけます。正規表現または配列関数を使用してそれを行うことができます。

これは読者への練習問題として残しておきます。また、章の最後には、クッキーを操作するためのヘルパー関数が掲載されています。

document.cookieへの書き込み

document.cookieに書き込むことができます。しかし、それはデータプロパティではなく、アクセサ(ゲッター/セッター)です。それへの代入は特別に処理されます。

document.cookieへの書き込み操作は、そこに記載されているクッキーのみを更新し、他のクッキーには影響しません。

たとえば、この呼び出しは、名前がuserで値がJohnのクッキーを設定します。

document.cookie = "user=John"; // update only cookie named 'user'
alert(document.cookie); // show all cookies

実行すると、複数のクッキーが表示される可能性があります。これは、document.cookie=操作がすべてのクッキーを上書きしないためです。言及されたクッキーuserのみを設定します。

技術的には、名前と値には任意の文字を含めることができます。有効なフォーマットを維持するには、組み込みのencodeURIComponent関数を使用してエスケープする必要があります。

// special characters (spaces) need encoding
let name = "my name";
let value = "John Smith"

// encodes the cookie as my%20name=John%20Smith
document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);

alert(document.cookie); // ...; my%20name=John%20Smith
制限事項

いくつかの制限事項があります。

  • document.cookieを使用して一度に1つのクッキーのみを設定/更新できます。
  • encodeURIComponent後のname=valueペアは4KBを超えてはなりません。そのため、クッキーには大量のデータを保存できません。
  • ドメインごとのクッキーの総数は約20個に制限されており、正確な制限はブラウザによって異なります。

クッキーにはいくつかの属性があり、その多くは重要であり、設定する必要があります。

属性はkey=valueの後にリストされ、次のように;で区切られています。

document.cookie = "user=John; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT"

ドメイン

  • domain=site.com

ドメインは、クッキーにアクセスできる場所を定義します。しかし実際には、制限があります。任意のドメインを設定することはできません。

別の2レベルドメイン(例:other.com)から、site.comで設定されたクッキーにアクセスできるようにすることはできません。

これは、1つのサイトでのみ使用できる機密データをクッキーに保存できるようにするための安全上の制限です。

デフォルトでは、クッキーはそれを設定したドメインでのみアクセスできます。

デフォルトでは、クッキーはforum.site.comなどのサブドメインと共有されません。

// if we set a cookie at site.com website...
document.cookie = "user=John"

// ...we won't see it at forum.site.com
alert(document.cookie); // no user

…しかし、これは変更できます。forum.site.comなどのサブドメインがsite.comで設定されたクッキーを取得できるようにしたい場合は、可能です。

そのためには、site.comでクッキーを設定する際に、ルートドメインにdomain属性を明示的に設定する必要があります:domain=site.com。そうすると、すべてのサブドメインがそのクッキーを参照できます。

例:

// at site.com
// make the cookie accessible on any subdomain *.site.com:
document.cookie = "user=John; domain=site.com"

// later

// at forum.site.com
alert(document.cookie); // has cookie user=John
レガシー構文

歴史的に、domain=.site.comsite.comの前にドットがある)は同じように機能し、サブドメインからクッキーへのアクセスを許可していました。ドメイン名の先頭のドットは現在無視されますが、一部のブラウザは、そのようなドットを含むクッキーの設定を拒否することがあります。

要約すると、domain属性により、サブドメインでクッキーにアクセスできるようにすることができます。

パス

  • path=/mypath

URLパスのプレフィックスは絶対パスでなければなりません。これにより、そのパス以下のページでクッキーにアクセスできるようになります。デフォルトでは、現在のパスです。

path=/adminでクッキーが設定されている場合、/adminおよび/admin/somethingのページでは表示されますが、/home/home/admin、または/では表示されません。

通常、すべてのWebサイトページからクッキーにアクセスできるようにするには、pathをルートに設定する必要があります:path=/。この属性が設定されていない場合、デフォルトはこの方法を使用して計算されます。

expires、max-age

デフォルトでは、クッキーにこれらの属性のいずれかが含まれていない場合、ブラウザ/タブを閉じると消えます。このようなクッキーは「セッションクッキー」と呼ばれます。

ブラウザの閉じてもクッキーを保持するには、expires属性またはmax-age属性を設定できます。両方が設定されている場合、max-Ageが優先されます。

  • expires=Tue, 19 Jan 2038 03:14:07 GMT

クッキーの有効期限は、ブラウザが自動的に削除する時刻を定義します(ブラウザのタイムゾーンに従います)。

日付は、GMTタイムゾーンでこのフォーマットで正確に指定する必要があります。date.toUTCStringを使用して取得できます。たとえば、クッキーを1日後に期限切れに設定できます。

// +1 day from now
let date = new Date(Date.now() + 86400e3);
date = date.toUTCString();
document.cookie = "user=John; expires=" + date;

expiresを過去の日付に設定すると、クッキーは削除されます。

  • max-age=3600

これはexpiresの代替であり、現在の時点からの秒数でクッキーの有効期限を指定します。

0または負の値に設定すると、クッキーは削除されます。

// cookie will die in +1 hour from now
document.cookie = "user=John; max-age=3600";

// delete cookie (let it expire right now)
document.cookie = "user=John; max-age=0";

secure

  • secure

クッキーはHTTPS経由でのみ転送する必要があります。

デフォルトでは、http://site.comでクッキーを設定すると、https://site.comにも表示され、その逆も同様です。

つまり、クッキーはドメインベースであり、プロトコルを区別しません。

この属性を使用すると、https://site.comによってクッキーが設定されている場合、同じサイトにHTTP(http://site.com)でアクセスしたときには表示されません。したがって、暗号化されていないHTTP経由で送信されるべきではない機密情報が含まれているクッキーの場合、secureフラグが適切です。

// assuming we're on https:// now
// set the cookie to be secure (only accessible over HTTPS)
document.cookie = "user=John; secure";

samesite

これは、別のセキュリティ属性samesiteです。これは、いわゆるXSRF(クロスサイトリクエストフォージェリ)攻撃から保護するために設計されています。

それがどのように機能し、いつ役立つのかを理解するために、XSRF攻撃を見てみましょう。

XSRF攻撃

bank.comサイトにログインしているとします。つまり、そのサイトから認証クッキーがあります。ブラウザは、ユーザーを認識し、すべての機密性の高い金融操作を実行するために、すべてのリクエストでbank.comに送信します。

さて、別のウィンドウでWebを閲覧しているときに、誤って別のサイトevil.comにアクセスしたとします。そのサイトには、ハッカーのアカウントへの取引を開始するフィールドを含む、bank.com<form action="https://bank.com/pay">フォームを送信するJavaScriptコードがあります。

フォームがevil.comから送信された場合でも、bank.comサイトにアクセスするたびに、ブラウザはクッキーを送信します。したがって、銀行はユーザーを認識し、支払いを処理します。

これは、いわゆる「クロスサイトリクエストフォージェリ」(略してXSRF)攻撃です。

もちろん、実際の銀行はそれらから保護されています。bank.comによって生成されたすべてのフォームには、悪意のあるページが生成したり、リモートページから抽出したりできない特別なフィールド、いわゆる「XSRF保護トークン」があります。悪意のあるページはそこにフォームを送信できますが、データを取り戻すことはできません。bank.comサイトは、受信するすべてのフォームでこのようなトークンをチェックします。

しかし、このような保護を実装するには時間がかかります。すべてのフォームに必要なトークンフィールドがあることを確認し、すべてのリクエストもチェックする必要があります。

クッキーのsamesite属性を使用する

クッキーのsamesite属性は、(理論的には)「xsrf保護トークン」を必要としない、そのような攻撃から保護する別の方法を提供します。

2つの可能な値があります。

  • samesite=strict

samesite=strictを持つクッキーは、ユーザーが同じサイトの外部から来た場合、決して送信されません。

言い換えれば、ユーザーがメールからリンクをクリックしたり、evil.comからフォームを送信したり、別のドメインから発信される操作を行ったりする場合、クッキーは送信されません。

認証クッキーにsamesite=strict属性がある場合、evil.comからの送信はクッキーなしで行われるため、XSRF攻撃が成功する可能性はありません。そのため、bank.comはユーザーを認識せず、支払いを処理しません。

保護は非常に信頼性が高いです。bank.comから来た操作のみがsamesite=strictクッキーを送信します(例:bank.comの別のページからのフォーム送信)。

ただし、小さな不便があります。

ユーザーがメモなどからbank.comへの正当なリンクをクリックすると、bank.comがユーザーを認識しないことに驚かれるでしょう。実際、この場合、samesite=strictクッキーは送信されません。

これは、2つのCookieを使用することで回避できます。「一般的な認識」用Cookie(「こんにちは、John」と表示するだけのもの)と、samesite=strictを付けたデータ変更操作用Cookieです。サイト外部からアクセスしたユーザーには歓迎メッセージが表示されますが、支払いは銀行のウェブサイトから開始する必要があります。そうすることで、2つ目のCookieが送信されます。

  • samesite=lax(値なしのsamesiteと同じ)

XSRF攻撃からも保護し、ユーザーエクスペリエンスを損なわない、より緩やかなアプローチです。

laxモードは、strictモードと同様に、サイト外部からのアクセスではCookieの送信を禁止しますが、例外が追加されています。

samesite=lax Cookieは、次の2つの条件が両方とも真である場合に送信されます。

  1. HTTPメソッドが「安全」である(例:GETだが、POSTではない)。

    安全なHTTPメソッドの完全なリストは、RFC7231仕様に記載されています。これらはデータの読み取りに使用すべきメソッドであり、書き込みには使用すべきではありません。データ変更操作を実行してはなりません。リンクをクリックする操作は常にGET(安全なメソッド)です。

  2. 操作がトップレベルのナビゲーションを実行する(ブラウザのアドレスバーのURLを変更する)。

    通常は真ですが、<iframe>内でナビゲーションが行われる場合は、トップレベルではありません。さらに、ネットワークリクエスト用のJavaScriptメソッドは、ナビゲーションを実行しません。

つまり、samesite=laxは、最も一般的な「URLに移動する」操作でCookieを許可します。たとえば、これらの条件を満たすメモからウェブサイトのリンクを開く場合などです。

しかし、別のサイトからのネットワークリクエストやフォームの送信など、より複雑な操作では、Cookieは失われます。

それで問題ない場合は、samesite=laxを追加しても、ユーザーエクスペリエンスが損なわれることはなく、保護機能が追加されます。

全体的に見て、samesiteは優れた属性です。

欠点があります。

  • samesiteは、2017年頃までの非常に古いブラウザでは無視されます(サポートされていません)。

そのため、保護のためにsamesiteのみに依存する場合、古いブラウザは脆弱になります。

しかし、XSRFトークンなどの他の保護策とsamesiteを併用して防御層を追加することで、将来、古いブラウザが消滅すれば、XSRFトークンを削除できるようになるでしょう。

httpOnly

この属性はJavaScriptとは関係ありませんが、完全性のために言及する必要があります。

ウェブサーバーは、Set-Cookieヘッダーを使用してCookieを設定します。また、httpOnly属性を設定することもできます。

この属性は、JavaScriptによるCookieへのアクセスを禁止します。document.cookieを使用して、そのようなCookieを参照したり操作したりすることはできません。

これは、ハッカーが独自のJavaScriptコードをページに挿入し、ユーザーがそのページにアクセスするのを待つ特定の攻撃から保護するための予防措置として使用されます。そもそもそのようなことは起こるべきではありません。ハッカーは当社のサイトにコードを挿入することはできませんが、それを可能にするバグが存在する可能性があります。

通常、そのようなことが発生し、ユーザーがハッカーのJavaScriptコードを含むウェブページにアクセスすると、そのコードが実行され、認証情報を含むユーザーCookieにアクセスするdocument.cookieへのアクセス権が得られます。これは危険です。

しかし、CookieがhttpOnlyの場合、document.cookieはそのCookieを参照しないため、保護されます。

付録:Cookie関数

ここでは、document.cookieを手動で変更するよりも便利な、Cookieを操作するための小さな関数セットを示します。

多くのCookieライブラリが存在するため、これらはデモ目的です。ただし、完全に機能します。

getCookie(name)

Cookieにアクセスする最も簡単な方法は、正規表現を使用することです。

getCookie(name)関数は、指定されたnameのCookieを返します。

// returns the cookie with the given name,
// or undefined if not found
function getCookie(name) {
  let matches = document.cookie.match(new RegExp(
    "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
  ));
  return matches ? decodeURIComponent(matches[1]) : undefined;
}

ここでnew RegExpは動的に生成され、; name=に一致します。

Cookieの値はエンコードされているため、getCookieは組み込みのdecodeURIComponent関数を使用してデコードすることに注意してください。

setCookie(name, value, attributes)

デフォルトでpath=/を使用して(他のデフォルトを追加するように変更できます)、Cookieのnameを指定されたvalueに設定します。

function setCookie(name, value, attributes = {}) {

  attributes = {
    path: '/',
    // add other defaults here if necessary
    ...attributes
  };

  if (attributes.expires instanceof Date) {
    attributes.expires = attributes.expires.toUTCString();
  }

  let updatedCookie = encodeURIComponent(name) + "=" + encodeURIComponent(value);

  for (let attributeKey in attributes) {
    updatedCookie += "; " + attributeKey;
    let attributeValue = attributes[attributeKey];
    if (attributeValue !== true) {
      updatedCookie += "=" + attributeValue;
    }
  }

  document.cookie = updatedCookie;
}

// Example of use:
setCookie('user', 'John', {secure: true, 'max-age': 3600});

deleteCookie(name)

Cookieを削除するには、有効期限を負の値で呼び出すことができます。

function deleteCookie(name) {
  setCookie(name, "", {
    'max-age': -1
  })
}
更新または削除には、同じパスとドメインを使用する必要があります。

注意:Cookieを更新または削除する際には、設定時とまったく同じパスとドメイン属性を使用する必要があります。

まとめ:cookie.js.

付録:サードパーティCookie

Cookieは、ユーザーが訪問しているページ以外のドメインによって配置された場合、「サードパーティCookie」と呼ばれます。

たとえば

  1. site.comのページが別のサイトからバナーを読み込む場合:<img src="https://ads.com/banner.png">

  2. バナーと共に、ads.comのリモートサーバーは、id=1234のようなCookieを含むSet-Cookieヘッダーを設定する場合があります。そのようなCookieはads.comドメインから発信され、ads.comでのみ表示されます。

  3. 次にads.comにアクセスすると、リモートサーバーはid Cookieを取得し、ユーザーを認識します。

  4. さらに重要なのは、ユーザーがsite.comから別のサイトother.comに移動した場合も、バナーがある場合、ads.comはCookieを取得します。これはads.comに属しているため、訪問者を認識し、サイト間を移動する際に追跡します。

サードパーティCookieは、その性質上、従来から追跡と広告サービスに使用されてきました。これらは発信元ドメインにバインドされているため、ads.comは、すべてのサイトがアクセスしている場合、異なるサイト間で同じユーザーを追跡できます。

当然のことながら、追跡されたくない人もいるため、ブラウザではそのようなCookieを無効にすることができます。

また、一部の最新のブラウザでは、そのようなCookieに対して特別なポリシーが採用されています。

  • Safariは、サードパーティCookieをまったく許可しません。
  • Firefoxには、サードパーティCookieをブロックするサードパーティドメインの「ブラックリスト」が付属しています。
注意

<script src="https://google-analytics.com/analytics.js">のように、サードパーティドメインからスクリプトを読み込み、そのスクリプトがdocument.cookieを使用してCookieを設定する場合、そのようなCookieはサードパーティCookieではありません。

スクリプトがCookieを設定する場合、スクリプトの出所がどこであっても、Cookieは現在のウェブページのドメインに属します。

付録:GDPR

このトピックはJavaScriptとはまったく関係ありませんが、Cookieを設定する際に覚えておくべき事項です。

ヨーロッパにはGDPRと呼ばれる法律があり、ウェブサイトがユーザーのプライバシーを尊重するためのルールを施行しています。これらのルールの1つは、ユーザーからの追跡Cookieに対する明示的な許可を求めることです。

これは、追跡/識別/認証Cookieに関することだけであることに注意してください。

そのため、ユーザーを追跡も識別もしない情報を保存するだけのCookieを設定する場合、自由に設定できます。

しかし、認証セッションまたは追跡IDを含むCookieを設定する場合、ユーザーがそれを許可する必要があります。

ウェブサイトは一般的に、GDPRへの準拠について2つの方法があります。ウェブ上で両方を見たことがあるでしょう。

  1. ウェブサイトが認証済みユーザーのみに追跡Cookieを設定する場合。

    そのためには、登録フォームに「プライバシーポリシーに同意する」(Cookieの使用方法について説明されている)チェックボックスを含める必要があります。ユーザーがそれをチェックすると、ウェブサイトは認証Cookieを設定できます。

  2. ウェブサイトがすべてのユーザーに対して追跡Cookieを設定する場合。

    合法的にこれを行うには、ウェブサイトは新規ユーザーに対してモーダルの「スプラッシュスクリーン」を表示し、Cookieに同意するよう求めます。その後、ウェブサイトはCookieを設定し、ユーザーにコンテンツを表示できます。ただし、新規訪問者にとっては煩わしい場合があります。コンテンツの代わりにそのような「必須クリック」モーダルスプラッシュスクリーンを見たい人はいません。しかし、GDPRは明示的な同意を要求しています。

GDPRはCookieだけでなく、他のプライバシー関連の問題にも関係していますが、それは私たちの範囲を超えています。

概要

document.cookieはCookieへのアクセスを提供します。

  • 書き込み操作は、その中に記載されているCookieのみを変更します。
  • 名前/値はエンコードする必要があります。
  • 1つのCookieのサイズは4KBを超えることはできません。ドメインで許可されるCookieの数は約20個(ブラウザによって異なります)。

Cookie属性

  • path=/は、デフォルトでは現在のパスであり、そのパスでのみCookieが表示されます。
  • domain=site.comは、デフォルトではCookieは現在のドメインでのみ表示されます。ドメインを明示的に設定すると、Cookieはサブドメインでも表示されるようになります。
  • expiresまたはmax-ageは、Cookieの有効期限を設定します。これらがなければ、Cookieはブラウザが閉じられると消えます。
  • secureは、CookieをHTTPS専用にします。
  • samesiteは、ブラウザがサイト外部からのリクエストでCookieを送信することを禁止します。これは、XSRF攻撃を防ぐのに役立ちます。

その他

  • ブラウザはサードパーティCookieを禁止する場合があります。たとえば、Safariはデフォルトでそうしています。Chromeにもこれを実装するための作業が進められています。
  • EU市民のために追跡Cookieを設定する場合は、GDPRで許可を求める必要があります。
チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点の提案がある場合は、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • いくつかのコードを挿入するには<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。