HTTPヘッダーとは

WebブラウザなどがWeb上のファイルやページを開くとき,ブラウザは以下のようなHTTPリクエストを作成しWebサーバーに送信する。

GET
scheme: https
host: alvine.org
filename: /

Webサーバーはこれに以下のようなHTTPレスポンスヘッダーにレスポンスボディ(ページ本体のデータ)を付け加えて応答する。大文字と小文字は区別されない。

HTTP/2 200 OK
alt-svc: h3=":443"; ma=2592000
cache-control: max-age=15768000
content-encoding: gzip
content-security-policy: default-src 'self'; frame-ancestors 'none';
content-type: text/html; charset=utf-8
cross-origin-embedder-policy: require-corp
cross-origin-opener-policy: same-origin
cross-origin-resource-policy: same-origin
etag: "d8uywr2ck2s594x-gzip"
last-modified: Tue, 01 Apr 2025 03:05:00 GMT
permissions-policy browsing-topics=(),interest-cohort=(),join-ad-interest-group=(),run-ad-auction=(),attribution-reporting=()
referrer-policy: strict-origin-when-cross-origin
server: Caddy
strict-transport-security: max-age=15768000;
vary: Accept-Encoding
x-content-type-options: nosniff
x-frame-options: DENY
content-length: 3442
date: Tue, 01 Apr 2025 12:34:19 GMT

レスポンスヘッダーの内容はWebサーバーの管理者が好きにいじることができる。Webサーバーアプリケーションのデフォルトのままのサイトが多いが,ここには利用者の役に立つ重要な情報を含めることができる。とくに私が重要と思うものを紹介したい。

セキュリティ系

だいたいCross-Origin なんとかの類。

セキュリティというと大仰に聞こえるが,結局レスポンスをどう処理するかはブラウザに委ねられているので「これだけ設定しておけば絶対安心」というものではない。本質的にはサーバーからの「お願い」にすぎないということを理解しておく必要がある。

Content-Security-Policy (CSP)

Content-Security-Policy: default-src 'self'; frame-ancestors 'none';

サードパーティーの要素読み込みを制限するヘッダー。XSS(クロスサイトスクリプティング)などを防ぐのに役立つ。

default-src 'self'にしておくとそのページと同一のホスト以外の要素(Webフォントや埋め込みSNSなど)は原則的にブロックされるのに加え,インラインの<style><script>もブロックされる。SVGもスタイルが効かなくなる。大抵のウェブサイトは使い物にならなくなるためかself一本でやっているサイトはほとんど見かけることはない。 小規模な個人ウェブサイトならすぐに導入できると思うが,このサイトみたいに動的なページ生成を行わずXSSを仕込みようがない場合は意味が薄い。

クリックジャッキングを予防することでおなじみのx-frame-options: DENYはすでに非推奨になっており,その機能はframe-ancestors 'none'に引き継がれた。

CSPは非常に奥が深い機能ではあるが,ここではとても解説しきれないので残りは割愛する。とりあえずdefault-srcだけでも設定しておくといいかもしれない。

Cross-Origin-Resource-Policy (CORP)

Cross-Origin-Resource-Policy: same-origin

別のオリジン(要するに他のサイト)からこのサイトのリソースを読み込むことを制限する。既定値はcross-originで,いかなるオリジンからのリクエストでもリソースの読み込みを許可する。これがsame-originsame-siteに設定されていれば他のサイトから直接画像やなどを読み込むことができなくなる。ただし,読み込みをブロックするのはあくまでブラウザ側の機能なので,必ずしもレスポンスボディが送信されないとは限らない。

Cross-Origin-Embedder-Policy (COEP)

Cross-Origin-Embedder-Policy: require-corp

“Embed”からもわかるように,指定したオリジン以外のリソースがこのサイト上で読み込まれるのを制限する。既定値はunsafe-noneで,この場合CORPで明示的に許可しなくてもクロスオリジンのリソースを取得することができる。require-corpに設定するとCORPで明示的に指定したオリジン以外の読み込みをブロックする。

Cross-Origin-Opener-Policy (COOP)

Cross-Origin-Opener-Policy: same-origin

このサイトが別のオリジンと閲覧コンテキストを共有することを制限する。既定値はunsafe-noneで,このサイトから他のオリジンが開かれたときに開かれた側のオリジンからJavaScriptのwindow.openerを利用して親ウィンドウを操作することができてしまう。same-originにするとオリジン間で閲覧コンテキストが共有されないので,お互いの通信はできなくなる。他のサイトによって操作される必要がないときはsame-originしておいたほうがいい。

決済サービスなどを利用する必要があるときはsame-origin-allow-popupsを使うらしい。

Strict-Transport-Security (HSTS)

Strict-Transport-Security: max-age=15768000;

ブラウザがHTTPで接続したとき,HTTPSにリダイレクトするように強制する。指定された秒数だけこのサイトに対するHTTPSへのリダイレクトが有効になる。よくある例はmax-age=15768000で半年,max-age=31536000で1年という設定例をよく見かける。ただしブラウザにキャッシュされないと意味がないので,初回アクセスやシークレットモードのアクセスに対してはmax-ageの期間を長くしたところで意味はない。

X-Content-Type-Options

X-Content-Type-Options nosniff

ブラウザがファイルのMIMEタイプを勝手に解釈しないようにする。古いIEではJSONファイルをスクリプトとして解釈してしまう問題があったらしい1

X-で始まるヘッダーは私的な非標準のものと,かつて非標準だったものが標準化したものがあるがこれは後者。そもそも私的なヘッダーをX-で始めること自体すでに非推奨になった2

プライバシー系

Referrer-Policy

Referrer-Policy: strict-origin-when-cross-origin

リクエストの際に送信されるrefererを制限する。

基本方針として同一オリジン間では完全なURLが得られたほうが検証がしやすいし,外部オリジンに対してはドメイン名くらいは送信してもいい。ただしHTTPSのオリジンからからHTTPのオリジンに対してはなるべく何も送信したくない。

このような場合,no-referrerにしてしまうと同一オリジン内でもリクエストの検証が難しくなってしまうので弊害が大きい。最適な選択肢はstrict-origin-when-cross-originだ。これはChromeやFirefoxをはじめ,ほとんどすべてのブラウザでデフォルトの設定値になっている。

Permissions-Policy

Permissions-Policy: browsing-topics=(), interest-cohort=(), join-ad-interest-group=(), run-ad-auction=(), attribution-reporting=()

これこそ「親切なHTTPヘッダー」の核心的な項目。

Permissions-Policyといえば位置情報やカメラの使用をWebサイト側から制限するヘッダーというイメージだが,これはブラウザのデフォルトの設定で許可を求めるようになっているはずだし個人サイトではそんなの使わないだろうから今回は割愛する。それよりももっと重要な設定項目がある。Googleのプライバシーサンドボックス機能のオプトアウトだ。

プライバシーサンドボックスとは

法規制が進みついにサードパーティーCookieの利用が封じられたGoogleが「サーバー側ではなくデバイス側で」利用者を追跡する技術のこと。現在開発中の技術ということでサンドボックスの名前がついているようだ。

どれをとっても邪悪としか言いようがない。たとえあなたのサイトがGoogleの広告のお世話になっているとしても無効にすべきだ。

ブラウザの設定からオプトアウトする手段はあるが,セキュリティやプライバシーに対して関心の薄い人たちを騙す意図をもはや隠そうともしていない。いくつかの機能はサーバー側のレスポンスヘッダーで(少なくともこのサイトについては)無効にすることができるので,すべての人類のために無効にしよう。

Topics API

Permissions-Policy: browsing-topics=()

Topics APIとはサイト横断的な利用者追跡の手段で,サードパーティーCookieに替わるものである。Googleは少し前にFLoC(Federated Learning of Cohorts)というユーザーを嗜好でグループ分けするシステムを発表して世界的な批判を浴びたが,Topics APIはその後継に当たるもののようだ。これはブラウザが利用者の閲覧履歴をもとに興味のあるカテゴリを列挙して保存しておき,Webサイト側の要求に応じてそれらのうち上位5つを参照できるようにするというもの。ただし5%の確率でランダムなカテゴリを返すという。

browsing-topics=()を設定することでサイト側は分析からオプトアウト3できる。

FLoC

Permissions-Policy: interest-cohort=()

interest-cohortは上で説明したFLoCのこと。これはすでにTopics APIに移行したようだが,いちおうinterest-cohort=()でオプトアウトできる。

Protected Audience API

Permissions-Policy: join-ad-interest-group=(), run-ad-auction=()

Protected Audience APIとはあらかじめ共通の関心をもつユーザーをインタレストグループというグループごとに分けておいて,彼らのデバイス側でWeb広告の入札オークションを行うシステムのことらしい。もとはFLEDGE(First“Locally-Executed Decision over Groups”Experiment / ローカルで実行される集団決定の初回実験)という名前で開発されていたが,2023年からProtected Audienceという名前に変わったようだ4

対象となるのは「過去に企業のサイトを閲覧したことがある利用者」で,その人たちに対して企業にリマーケティングの機会を与えるのが目的だという。

join-ad-interest-group=()でブラウザをインタレストグループに追加することを無効にできる5run-ad-auction=()でオークションを実行する機能を無効にできるが,こちらは広告のスクリプトを読み込んでいなければ関係ないかもしれない。ただし具体的に何のデータをもとにインタレストグループへの追加を行うのかは調べてもよくわからなかった。(このあたりはTopics APIに似ている気もするが説明がなく不明。)

Attribution Reporting API

Permissions-Policy: attribution-reporting=()

Attribution Reporting APIとは「広告の閲覧がいつ購入につながったか」という測定結果をブラウザがあとで広告プラットフォームに送信する仕組み。このサイトのように一切広告を利用していないサイトには関係なさそうに思えるが,将来的に広告以外に利用することもほのめかされている6ので,今のうちに無効7にしておいてもいいくらいかもしれない。

トラフィック削減

Cache-Control

Cache-Control: max-age=15768000

ブラウザはすべてのHTTPリクエストを送信するわけではなく,自分自身に格納されたキャッシュからレスポンスを作成することがある。max-ageを使ってレスポンスを再利用可能な期間をサーバー側から通知しておくことで,クライアントの余計なトラフィックの負荷を軽減することができる。

max-ageを長めに設定したとしてもサイトの更新が利用者に伝わらないという心配は無用だ。ブラウザはレスポンスヘッダーのlast-modifiedetagの値から自身のキャッシュと受け取るレスポンスボディが同一であるかどうかを判断し,同一であった場合はステータスコード304 Not Modifiedを返してキャッシュからレスポンスボディを作成するためだ。サーバーのログを確認して,304が多いと感じたら長めに設定しておいたほうがいい。ChromeのLighthouseテストを実行したときも適切なキャッシュ期間を設定するようにアドバイスがあるはずだ。

さらに,最近のブラウザは賢いので「1年間更新のなかったサイトはその後も更新される可能性が低い」と考えて,last-modifiedから1年以上経過した,とうにmax-ageを過ぎたキャッシュを再利用することがある。これは「ヒューリスティックキャッシュ」と呼ばれる。

トラブルシューティング

さて,ここまでまあまあ厳しめの設定方法を紹介してきたが,厳格な設定はときに可用性を損なってしまう。

HTTPヘッダーの設定に起因する実際のトラブルを紹介したい。

SVGが真っ黒な画像に

このサイトのfaviconといえばこれ。見たことあるよね。

Content-Security-Policyを設定してからしばらく経った日のこと。ふと「faviconはちゃんと表示されるかな?」と思いブラウザで開いてみた。どれも正常に表示されているのだが,favicon.svgだけは真っ黒な正方形として表示されてしまった。

はじめはブラウザのダークモードがなにか悪さをしているのではと考えてオフにして開き直したりもしてみたが,結果は変わらず。ダウンロードしてテキストエディタで開くことでようやくその原因が掴めた。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
    version="1.1"
    id="svg1"
    width="256.00003"
    height="256.00003"
    viewBox="0 0 256.00003 256.00003"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:svg="http://www.w3.org/2000/svg">
    <style id="style1">
        path {
            fill: #000;
        }
        @media (prefers-color-scheme: dark) {
            path {
                fill: #fff;
            }
        }
    </style>
    <defs id="defs1" />
    <rect
        style=""
        id="rect1"
        width="256"
        height="256"
        x="0"
        y="0" />
    <path
        style=""
        d="M 0,127.99999 (省略) "
        id="path3" />
</svg>

rectとpathのstyleだけが抜け落ちてしまっている。なぜかstyle1は残っているというのは気になるが(ダークモードでも反転しないつもりなのでむしろ必要ないのだが……),結局image/svg+xmlに対するリクエストについてだけContent-Security-Policy: style-src 'unsafe-inline';を返すようにしたら解消した。

base64画像が表示されない

CSPに関連してもう1件。このサイト上のあるページにbase64でエンコードされた画像を設置しているのだが,CSPをdefault-src: 'self'としていると表示されなかった。表示させるにはimg-src 'self' data:;とスキームを指定して追加する必要があった。

TwitterのOGP

このサイトではイラストのページはSNS(とくにTwitter)で共有することを考えてOGP(Open Graph Protocol)に基づいたメタデータを設定している。これをやっておくといわゆる「Twitterにウェブサイトを埋め込むやつ」ができる。

OGP画像の例

こんなやつ

しかし,このサイトではこのように記事のメタデータは読み込まれてもサムネイル画像だけが読み込まれないトラブルがあった。

公式のOGP Validatorは廃止されたので,ツイート作成画面で確認している。

このサイトのサムネイルは160x160で統一しているが,これは普通のOGP画像(1200x630が多い)と比べてかなり小さいため最小サイズの問題かと思って試行錯誤してみたが解決せず8。面倒なのでしばらく放置していたが,これも「ひょっとして」と思いたちサムネイル用画像の置いてあるディレクトリのCORPをCross-Origin-Resource-Policy: cross-originとしてみたところ,画像が表示された。

サムネイルがちゃんと表示されている。

参考にさせていただいたサイト

具体的な出典は脚注に記述。


  1. 機密情報を含むJSONには X-Content-Type-Options: nosniff をつけるべき - 葉っぱ日記↩︎

  2. RFC 6648 - Deprecating the “X-” Prefix and Similar Constructs in Application Protocols↩︎

  3. サイトの特定のページのトピック計算をオプトアウトするには、ページに Permissions-Policy: browsing-topics=() Permissions-Policy ヘッダーを含めると、、そのページに限りすべてのユーザーのトピックの推論が防止されます。サイトの他のページへのその後のアクセスには影響しません。あるページで Topics API をブロックするポリシーを設定しても、他のページには影響しません。

    Topics API 開発者ガイド  |  Privacy Sandbox  |  Google for Developers↩︎

  4. Protected Audience API: Our new name for FLEDGE↩︎

  5. Protected Audience API を無効にする  |  Privacy Sandbox  |  Google for Developers↩︎

  6. 注: 今後、Attribution Reporting API は広告とは関係のないユースケースに利用されるようになる可能性があります。

    ウェブ向けアトリビューション レポートの概要  |  Privacy Sandbox  |  Google for Developers↩︎

  7. サイトは HTTP レスポンス ヘッダーを送信することで、トップレベルのアクセス権を持つスクリプトを含め、すべての関係者の Attribution Reporting API を無効にできます Permissions-Policy: attribution-reporting=()

    ウェブ向けアトリビューション レポートの概要  |  Privacy Sandbox  |  Google for Developers↩︎

  8. そもそもTwitterのドキュメントにはサイズにまつわる制限は見当たらなかった。 | Cards markup | Docs | Twitter Developer Platform↩︎