30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Cookie 概説

Cookie とは、HTTP でステートフルなやり取りを実現するために、ブラウザとサーバ間で情報を送受信する仕組みである。

HTTP は本来ステートレスなプロトコルである。そのため、同一のユーザーが連続でリクエストを行っても、それぞれ独立したリクエストであり、「同じユーザーからのリクエストである」とサーバが認識することはできない。
これは例えば、ログイン状態の管理で問題となる。ID とパスワードで認証を行っている場合、リクエストの度に ID とパスワードを送信しなければならない。

Cookie を使うことで、このような事態を解決できる。

まず、サーバがブラウザに対して、Cookie としてどのような情報を保存するのか指示する。具体的には、レスポンスヘッダにSet-Cookieフィールドを含め、そこに Cookie として保存させたい情報を設定する。ログイン状態を管理したい場合は、セッショントークンなどを設定することになる。
このレスポンスを受け取ったブラウザは、サーバからの指示通りの内容で、Cookie を保存する。
そして再びそのサーバに対してリクエストを送る際に、ブラウザが自動的に、Cookie で保存していた情報をリクエストヘッダのなかに含める。そのためサーバは、例えばセッショントークンなどを取得でき、都度 ID やパスワードを受け取らなくてもどのユーザーからのアクセスなのかを認識できる。

このような仕組みで Cookie はステートフルなやり取りを実現させており、セッション管理以外にも、アクセス解析やパーソナライゼーションなどに利用されている。

便利な半面、セキュリティ上のリスクもある。
先程説明したような形で Cookie にセッショントークンを保存しているウェブアプリケーションは多く、Cookie の内容を盗まてしまうと、第三者による成りすましが行われる恐れがある。
また、リクエストの際に自動的に Cookie が付与されるという仕組みを悪用して、ユーザーが意図していない操作を行わせるという攻撃も存在する。この攻撃が成功してしまうと例えば、SNS への不適切な投稿やパスワードの変更などが、ユーザーの知らない間に行われてしまう可能性がある。

また近年は、プライバシー保護の観点から、Cookie の取り扱いが問題になることも多くなってきている。
どのようなサイトを見ているのか、どのような広告をクリックし、何を買ったのか。そういった情報を特定することが、Cookie を使えば可能になるからだ。
EU では Cookie に関する規制が既に存在しており、日本でもリクルートによる Cookie の悪用が大きな話題となった。

このように Cookie は、現在のウェブにとって重要な機能であり、かつ、慎重な取り扱いが求められる機能でもある。
だからこそ、どのような仕組みなのか正しく理解することが重要になる。

Cookie の基本的な機能はRFC 6265で定義され、各ブラウザベンダが実装している。
同じブラウザでも、バージョンアップによって Cookie の取り扱いが変更になる場合がある。プライバシー保護という観点から Cookie への規制が強まっていることは既に書いたが、ブラウザの実装においても、Cookie の利用を制限していこうという傾向にある。

この記事では、ローカル環境で実際に Cookie の操作を行いながら、具体的な挙動を確認していく。
動作環境は、サーバは Node.js のv12.14.1、ブラウザは Google Chrome の79.0.3945.130

まずは基本的な機能を確認し、次にRFC 6265で定義されている各属性について調べていく。

Set-Cookie フィールドと Cookie フィールドの基本

以下のコードを Node.js で実行するとhttp://localhost:8080というサーバが起動するので、ブラウザの開発者ツールのNetworkタブを開いた状態でアクセスする。

const http = require('http');

http.createServer((req, res) => {
  res.setHeader('Set-Cookie', [
    'a=xxx',
    'b=yyy',
  ]);
  res.writeHead(200);
  res.end('Hello World');
}).listen(8080);

Hello Worldと表示されているのを確認し、次は、開発者ツールで HTTP のやり取りを確認する。
以下が、リクエストとレスポンスのヘッダ。今回の記事と無関係なフィールドについては基本的に省略している。

GET / HTTP/1.1
Host: localhost:8080
HTTP/1.1 200 OK
Set-Cookie: a=xxx
Set-Cookie: b=yyy

レスポンスにSet-Cookieフィールドが 2 つあり、その値はそれぞれa=xxxb=yyyになっている。
Cookie は名前=値という形式で指定するので、これでブラウザにはabの 2 つの Cookie が保存された。

ブラウザのページをリロードしてhttp://localhost:8080に再びアクセスする。
すると今回のリクエストヘッダには、初回のアクセス時には無かったCookieフィールドが存在している。

GET / HTTP/1.1
Host: localhost:8080
Cookie: a=xxx; b=yyy

このようにブラウザは自動的に、保存しておいた Cookie をサーバに送信する。

コードを以下のように書き換えて再度サーバを起動してアクセスすると、サーバ側で Cookie を取得できていることを確認できる。

const http = require('http');

http.createServer((req, res) => {
  console.log(req.headers.cookie); // a=xxx; b=yyy
  res.writeHead(200);
  res.end('Hello World');
}).listen(8080);

JavaScript によるブラウザ側での Cookie の操作

Cookie は、JavaScript を使ってブラウザ側で操作することも可能。
試しにブラウザのコンソールを開いて以下のコードを実行すると、Cookie の内容が書き換わる。

> document.cookie
"a=xxx; b=yyy"
> document.cookie = 'a=foo'
"a=foo"
> document.cookie
"b=yyy; a=foo"

この状態でブラウザをリロードすると、先程はa=xxx; b=yyyだったreq.headers.cookieb=yyy; a=fooになっている。

Set-Cookie による上書き

既に存在する Cookie と同じ名前の Cookie をSet-Cookieで指定した場合、その内容で上書きされる。

以下のサーバにアクセスすると、ブラウザに保存されている Cookie の内容はa=xxx; b=yyyになる。

const http = require('http');

http.createServer((req, res) => {
  res.setHeader('Set-Cookie', [
    'a=xxx',
    'b=yyy',
  ]);
  res.writeHead(200);
  res.end('Hello World');
}).listen(8080);

その後、サーバを以下の内容に書き換えてから起動し直した上で、改めてアクセスする。すると、Cookie 内容はb=yyy; a=123になる。

const http = require('http');

http.createServer((req, res) => {
  res.setHeader('Set-Cookie', [
    'a=123',
  ]);
  res.writeHead(200);
  res.end('Hello World');
}).listen(8080);

Expires と Max-Age

ここからは、RFC 62655.2に書かれてある 6 つの属性について見ていく。
属性も、保存するデータと同じように属性名=値という形式で設定する。

まず、ExpiresMax-Age
予め Google Chrome の設定画面から、設定済みのhttp://localhost:8080の Cookie を削除しておく。

これらは Cookie の生存期間に関する属性。Expiresでは日時を、Max-Ageでは秒数をそれぞれ指定し、それを経過すると自動的にブラウザから削除される。
以下の例では、aは指定された日時を過ぎたら、bは 10 秒が経過したら、ブラウザから削除される。

const http = require('http');

http.createServer((req, res) => {
  res.setHeader('Set-Cookie', [
    'a=xxx; Expires=Fri, 17 Jan 2020 15:00:00 GMT',
    'b=yyy; Max-Age=10',
  ]);
  res.writeHead(200);
  res.end('Hello World');
}).listen(8080);

Expiresに過去の日付を指定したり、Max-Ageに 0 やマイナスの数値を指定したりすると、その Cookie は保存されない。そして既述したように、Cookie の内容は常にSet-Cookieで上書きされる。
そのため、既に存在する Cookie の名前で過去の時間を設定すると、その Cookie はブラウザから消去されることになる。

Domain と Path

Cookie はリクエストに自動的に付与されるが、その挙動を制御するための属性がDomainPath

Domainはその名の通り、Cookie を送るドメインを指定する。
だが現実的には、この属性を設定することはほとんどないはず。
設定しない場合、Cookie を発行しているサーバのドメインがそのまま対象になり、それで問題ないからだ。

むしろDomainを設定すると Cookie を送付する対象が広がってしまい、脆弱性が生まれる恐れすらある。
具体的には、Domain属性に指定したドメインだけでなく、そのサブドメインにも Cookie が送られるようになってしまう。

ローカル環境でsub.localhostのようなサブドメインを用意して動かしたが、思ったような挙動にならなかった。
だがこの機会にきちんと確認しておきたかったので、本番環境に動作確認用のページを用意して確認した。

具体的には、次の手順で確認する。

  1. ブラウザの開発者ツールを開き HTTP 通信の内容を確認できる状態にしておく
  2. https://numb86.net/ にアクセスする
    • そうすると、noSpecifyDomainCookiespecifyDomainCookieの 2 つの Cookie がセットされる。JavaScript でセットしているためレスポンスヘッダには含まれないので、注意する。
    • どちらもMax-Ageを 180 秒にしてあるので、ブラウザに残り続ける心配はない。逆に言えば 180 秒以内に以下の手順を行わないと、正しく検証できない。
    • noSpecifyDomainCookieにはDomain属性を設定せず、specifyDomainCookieにはDomain属性としてnumb86.netを設定している。
  3. ページをリロードして、リクエストヘッダの内容を確認する
    • CookieフィールドでnoSpecifyDomainCookiespecifyDomainCookieの両方を送信していれば、Cookie がブラウザに正しく保存されていることになる。
  4. サブドメインである https://ken-all.numb86.net/ にアクセスして、リクエストヘッダのCookieフィールドで何が送信されているか確認する
    • レスポンスはステータスコード404になるが、リクエストの内容には影響しないため問題ない
    • specifyDomainCookieは送信されているが、noSpecifyDomainCookieは送信されていない

つまり、Domainを指定した Cookie のみがサブドメインにも送信されていることを確認できた。

Cookie は Web Storage とは異なりオリジンではなくドメインで管理している。そのため、プロトコル(http or https)やポート番号が違っても、ドメインが同じなら Cookie は送信されるので注意する。

Path属性を指定すると、指定したパス以下の URL にアクセスした場合にのみ、Cookie が送信されるようになる。指定しなかった場合は/を指定したのと同じ扱いになり、どの URL であっても送信される。
以下のcという Cookie は、http://localhots:8080/foo/bar以下のパスにアクセスした場合にのみ、リクエストヘッダのCookieフィールドに含まれる。

const http = require('http');

http.createServer((req, res) => {
  res.setHeader('Set-Cookie', [
    'a=111; Max-Age=60; path=/',
    'b=222; Max-Age=60; path=/foo',
    'c=333; Max-Age=60; path=/foo/bar',
    'd=444; Max-Age=60;',
  ]);
  res.writeHead(200);
  res.end('Hello World');
}).listen(8080);

Secure

Secure属性が設定された Cookie は、HTTPS 接続の場合にのみ、サーバに送られる。

この挙動を確認するため、自己署名証明書を用意してローカル環境に HTTPS サーバを立てる。
OpenSSLを使って秘密鍵(server.key)と証明書(server.crt)を作成した。OpenSSLの使い方はこの記事の範囲を越えるので割愛する。

そして、サーバのコードを以下のようにする。

const fs = require('fs');
const http = require('http');
const https = require('https');

const options = {
  key : fs.readFileSync('./server.key'),
  cert: fs.readFileSync('./server.crt')
};

https.createServer(options, (req, res) => {
  res.setHeader('Set-Cookie', [
    'secureCookie=111; Max-Age=60; Secure',
    'noSecureCookie=222; Max-Age=60',
  ]);
  res.writeHead(200);
  res.end('Hello World');
}).listen(8443);

http.createServer((req, res) => {
  res.writeHead(200);
  res.end('Hello World');
}).listen(8080);

このファイルを実行すると、https://localhost:8443http://localhost:8080の 2 つのサーバが起動する。

まずhttps://localhost:8443にアクセスする。すると、以下のレスポンスヘッダが返ってくる。

HTTP/1.1 200 OK
Set-Cookie: secureCookie=111; Max-Age=60; Secure
Set-Cookie: noSecureCookie=222; Max-Age=60

もう一度https://localhost:8443にアクセスすると、secureCookienoSecureCookieの両方がリクエストヘッダに含まれている。

GET / HTTP/1.1
Host: localhost:8443
Cookie: secureCookie=111; noSecureCookie=222

続いてhttp://localhost:8080にアクセスすると、noSecureCookieのみがリクエストヘッダに含まれている。

GET / HTTP/1.1
Host: localhost:8080
Cookie: noSecureCookie=222

HttpOnly

HttpOnly属性が設定された Cookie は、JavaScript からアクセスすることができなくなる。

以下のサーバではoperableWithJsnoOperableWithJsがレスポンスヘッダで渡されるが、JavaScript でアクセスできるのはHttpOnlyが設定されていないoperableWithJsのみである。

const http = require('http');

http.createServer((req, res) => {
  res.setHeader('Set-Cookie', [
    'operableWithJs=111; Max-Age=60;',
    'noOperableWithJs=222; Max-Age=60; HttpOnly',
  ]);
  res.writeHead(200);
  res.end('Hello World');
}).listen(8080);
> document.cookie
"operableWithJs=111"

XSS という、ウェブアプリケーションをターゲットにした有名な攻撃方法があるが、これは攻撃者が用意した JavaScript コードをユーザーのブラウザで実行させる手口である。
XSS が成功してしまうと例えば、ユーザーのブラウザに保存されている Cookie が盗み出されてしまう。
Cookie にHttpOnly属性を設定しておくことで、少なくとも保存済みの Cookie を JavaScript によって盗まれてしまうことは防げる。

SameSite

SameSiteはまだ提案段階であり RFC で定義されておらず、今のところRFC 6265にも含まれていない。
だが各ブラウザベンダによる実装は着々と進んでおり、多くのブラウザで実際に使用できる。

参考:
Cookieの仕様改定版、RFC6265bisの議論 - ASnoKaze blog
Can I use... Support tables for HTML5, CSS3, etc

セキュリティ的にかなり重要な機能なので、SameSite属性については別途記事を書く予定。

CSS で斜線を描画する

HTML や CSS に、斜線を描画するための仕組みは用意されていない。
だがlinear-gradientを利用することで、このような斜線を描画できる。

f:id:numb_86:20200106013247p:plain

「Can I use...」で確認すると、主要なブラウザは全てlinear-gradientに対応している。

linear-gradientはその名の通り、線形グラデーションを表現するためのスタイル。
なぜそれを使って斜線を描画できるのか理解するために、まずはlinear-gradientの機能を説明する。

以降の例では全て<div></div>という空のdiv要素を用意し、そこにスタイルをあてていく。
説明しやすいからそうしただけであり、linear-gradientが機能する要素ならどこにでも斜線を描画できる。

まず、以下のようたスタイルを定義してみる。

div {
  width: 100px;
  height: 100px;
  background-image: linear-gradient(0deg, skyblue, greenyellow);
}

すると、以下のように表示される。

f:id:numb_86:20200106012636p:plain

100*100の要素が表示され、上方向に、青から緑へとグラデーションになっている。
この画像と CSS を見比べると、linear-gradientの仕組みを理解しやすい。

ひとつめのパラメータである0degはグラデーションの方向を角度で指定している。0degは上方向を意味する。90degは右方向、180degは下方向、270degは左方向を意味する。
残りのパラメータは開始色と終了色。
そのため今回の例では、「上方向に向かって、skyblueからgreenyellowへとグラデーションさせる」という意味になる。

ひとつめのパラメータについては、角度ではなく、方向を表すキーワードを使うこともできる。
例えばto rightとすれば右方向に、to right topとすれば右上方向に、グラデーションさせる。

f:id:numb_86:20200106012648p:plain

このグラデーションを使って、どうやって斜線を表現するのか。重要なのが、「途中色」という概念である。

linear-gradientで指定できるのは、2色だけではない。開始色と終了色の他に途中色を指定して、複数の色でグラデーションさせることができる。

div {
  width: 300px;
  height: 100px;
  background-image: linear-gradient(90deg, black, white, skyblue, black);
}

f:id:numb_86:20200106012713p:plain

この例では、右方向に、黒 -> 白 -> 青 -> 黒とグラデーションさせている。

画像を見れば分かるように、デフォルトだと均等な間隔でのグラデーションになる。
この間隔を指定することもできる。

div {
  width: 300px;
  height: 100px;
  background-image: linear-gradient(90deg, black 20%, white 30%, skyblue 90%, black);
}

f:id:numb_86:20200106012727p:plain

0%から20%までが黒で、そこから10%分の領域が白へのグラデーションとなる。
そこから60%分の領域が青へのグラデーションとなり、残りの10%が青から黒へのグラデーションとなる。

これを応用することで、線を描画することができる。

div {
  width: 300px;
  height: 100px;
  background-image: linear-gradient(90deg, transparent 44%, black 44%, black 56%, transparent 56%);
}

f:id:numb_86:20200106012738p:plain

transparentは透明を意味するので、0%から44%地点までが透明である。
そして同じ44%地点で黒に切り替えるが、56%地点でも黒を指定している。そのため、この12%分の領域は黒で塗り潰される。
そして56%地点で再び黒から透明に切り替え、あとは最後まで透明が続く。
これが、「線」が表示される理由である。

あとはこれを傾ければ、斜線になる。

div {
  width: 300px;
  height: 100px;
  background-image: linear-gradient(to right top, transparent 44%, black 44%, black 56%, transparent 56%);
}

f:id:numb_86:20200106012750p:plain

要素の大きさやlinear-gradientの指定によって、斜線の太さ、角度、色を、自由に調整できる。

f:id:numb_86:20200106012801p:plain

***

個人的な感想だが、この手法はだいぶ無理をしている感がある。
ブラウザは本来ドキュメントビューアであり、HTML は文書構造を表現するためのものであり、CSS はそれを装飾するためのものだった。
それがいつの間にか、アプリケーションのプラットフォームとして使われるようになっていった。本来とは異なる用途での利用が盛んになった。
だからどうしても、アプリケーションを開発する上での機能が貧弱になる。アプリケーションプラットフォームとして考えると「斜線を描画する機能もないのか」となるが、その出自を考えれば仕方がない。
だから今回のように、本来とは違う使い方をして、やりたいことを実現していくことになる。CSS は特にその傾向が強い印象がある。
Web やブラウザが本来とは違う使い方をされていて、苦し紛れというか、あまり健全ではないような気もするが、それくらいインターネットや Web の仕組みが便利だったのだろう。

参考資料