0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HTTPSでも守れないものがある — セキュリティヘッダを図解する

0
Posted at

この記事は riku-kobayashi.jpに掲載した記事の転載です。

TLSハンドシェイクや証明書チェーンの記事で、HTTPSが通信路をどう守っているかを見てきました。

ただ、HTTPSにしただけでは守れないものがあります。TLSが守るのは通信路であって、ブラウザがレスポンスをどう扱うかではないからです。

その穴を埋めるのが、レスポンスヘッダでブラウザに出す指示、いわゆるセキュリティヘッダです。この記事では主要な5つを、何を防ぐためのヘッダなのかという観点で図解します。

TLSが守る範囲と、守らない範囲

TLSが保証してくれるのは、サーバーとブラウザの間で盗聴・改ざんされないことと、相手が本物のサーバーであることです。

逆に言うと、データがブラウザに届いた後のことには関与しません。

  • 届いたHTMLに紛れ込んだ悪意あるスクリプトを実行するかどうか
  • 別のサイトが自分のページをiframeで埋め込んでよいか
  • 外部サイトへ遷移するときに、元のURLをどこまで教えるか

こういった判断は全部ブラウザ任せです。しかもブラウザは、サーバーから指示がなければ寛容なデフォルトで動くことが多いんですよね。

セキュリティヘッダは、右側のブラウザの中に対して、サーバー側からこの条件で動いてくれと伝える仕組みです。

5つのヘッダ早見表

先に全体像を置いておきます。

ヘッダ 防ぎたいもの 指示の中身
Strict-Transport-Security HTTPへの降格(SSLストリップ)1 このサイトは今後HTTPSでしか開くな
X-Frame-Options クリックジャッキング2 このページをiframeに入れるな
Content-Security-Policy XSS(悪意あるスクリプトの実行) 読み込んでよいリソースの出所はここだけ
X-Content-Type-Options MIMEスニッフィング3 Content-Typeを信じろ、中身から推測するな
Referrer-Policy URLに含まれる情報の漏洩 遷移先に渡すRefererを絞れ

ここから1つずつ見ていきます。

Strict-Transport-Security (HSTS)

TLSの記事の延長で、いちばん繋がりが強いのがこれです。

Strict-Transport-Security: max-age=31536000; includeSubDomains

HTTPSにしていても、最初の1回が危ない

ユーザーがアドレスバーにexample.comとだけ打つと、ブラウザはまずhttp://でアクセスします。サーバーは301でhttps://に飛ばしてくれるのですが、この最初のHTTPリクエストは暗号化されていません。

同じネットワーク上の攻撃者がここに割り込めば、リダイレクトを握りつぶして、ユーザーを偽サイトや平文の通信に留め置けます。これがSSLストリップです。

HSTSで何が変わるか

HSTSのヘッダを一度受け取ると、ブラウザはmax-ageで指定された秒数のあいだ、そのドメインへのアクセスをURLがhttpでも内部でhttpsに書き換えてから送ります。

つまり2回目以降は、平文のリクエストがそもそもネットワークに出ません。リダイレクトを待たずに済むので、割り込む隙もなくなります。

弱点と注意点

HSTSには弱点がひとつあって、最初の1回は保護されないことです。ヘッダを受け取るまでブラウザは何も知らないので。

これを埋めるのがプリロードリストで、ブラウザ自体にHSTS対象ドメインの一覧を内蔵してもらう仕組みです。登録にはpreloadディレクティブ付きのヘッダが必要です。

プリロード登録とは

自分のドメインを、ブラウザに最初から埋め込まれているHSTS対象ドメインの一覧に載せてもらうことです。

登録はhstspreload.orgから申請します。条件を満たしたドメインは、ブラウザの次のリリースに組み込まれる形で反映されます。Chromeの場合、安定版に載るまで数か月かかります。

ヘッダのpreloadは、この一覧に載せることへの同意を示す印で、付けただけでは何も起きません。

一度載ると、解除もブラウザのリリース待ちになります。

ただ、includeSubDomainsとpreloadは軽い気持ちで付けないほうがいいです。

  • includeSubDomainsは全サブドメインがHTTPS対応していないと、HTTPでしか動かないサブドメインが開けなくなる
  • 一度ブラウザに覚えさせたmax-ageは、サーバー側からは取り消せない
  • プリロードリストからの削除は、各ブラウザのリリースを待つので時間がかかる

最初は短いmax-ageで始めて、問題が出ないことを確認しながら延ばしていくのが無難だと思います。

X-Frame-Options

X-Frame-Options: DENY

クリックジャッキング

攻撃者のページが、正規サイトのページを透明なiframeで重ねて表示します。ユーザーは攻撃者のページのボタンを押したつもりで、実際には下にある正規サイトの購入ボタンや削除ボタンを押している、という攻撃です。

値

X-Frame-Optionsの値は2つです。

  • DENY: どこからもiframeに入れさせない
  • SAMEORIGIN: 同じオリジンからなら許可

Content-Security-Policy (CSP)

セキュリティヘッダの中で、威力も面倒くささも一番大きいのがこれです。

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

何をするヘッダか

XSS4は、入力のエスケープ漏れなどでページに紛れ込んだ<script>が実行されてしまう攻撃です。

エスケープで防ぐのが本筋ですが、1か所でも漏れたら終わりです。CSPは漏れた場合の二重の防壁として、ブラウザ側に実行・読み込みを許可するリソースの出所を教えます。

攻撃者が<script src="https://evil.example/x.js">を差し込めたとしても、script-src 'self'ならブラウザが読み込みを拒否します。

ブラウザはスクリプトを取得する前に、そのURLのオリジンをCSPの許可リストと照合します。evil.exampleは'self'(自分と同じオリジン)に当てはまらないので、リクエストを送ることなくブロックされます。

判定するのはブラウザで、基準になるのはサーバーが送ったCSPヘッダです。攻撃者はHTMLにタグを差し込めても、このヘッダまでは書き換えられません。

主なディレクティブ

  • script-src / style-src / img-src / connect-src: 種別ごとの許可元
  • default-src: 上のような種別ごとの指定に当てはまらない場合に適用される
  • frame-ancestors: このページを埋め込んでよい親ページ(前述のX-Frame-Optionsの後継)

'self'は自分と同じオリジン、'none'は一切許可しない、という意味です。

frame-ancestorsは、前の節のX-Frame-Optionsをより細かく制御できるようにした後継です。frame-ancestors 'none'はDENY、frame-ancestors 'self'はSAMEORIGINに相当し、さらに特定のオリジンだけを許可することもできます。両方が指定されたら、対応しているブラウザではframe-ancestorsが優先されます。

とはいえ古いブラウザも考えて、今も両方付けておくのが一般的なようです。

Content-Security-Policy: default-src 'self'; img-src 'self' https://images.example.com; frame-ancestors 'none'
ディレクティブ 対象 実際に使われる指定 許可される出所
script-src スクリプト default-src(個別指定なし) 自分だけ
style-src CSS default-src(個別指定なし) 自分だけ
img-src 画像 img-src(個別指定あり) 自分とimages.example.com
connect-src fetchなどの通信先 default-src(個別指定なし) 自分だけ
frame-ancestors このページを埋め込む親ページ frame-ancestors(個別指定あり) どこからも不可

img-srcとframe-ancestorsは専用の指定があるので、そちらが使われ、default-srcは見られません。それ以外は専用の指定がないので、default-src 'self'が適用されます。

専用のディレクティブを書いた種別では、default-srcは足し合わせではなく無視されます。script-src https://cdn.example.comとだけ書くと、自分のオリジンのスクリプトまで許可されなくなる点に注意が必要です。

面倒なのはインラインスクリプト

CSPを真面目に設定すると、まずインラインの<script>やonclick="..."が全部動かなくなります。インラインスクリプトはXSSの主な入口なので、それを止めるのがCSPの狙いの中心だからです。

ただ、攻撃者が差し込んだインラインスクリプトと、自分で書いたインラインスクリプトは、HTML上では見分けがつきません。どうしても必要なら、自分のものにだけ目印を付けて許可します。目印の付け方は2通りです。

nonce

サーバーがリクエストのたびにランダムな文字列を作り、CSPヘッダと、許可したい<script>タグの両方に入れます。

Content-Security-Policy: script-src 'nonce-r4nd0m123'
<script nonce="r4nd0m123">console.log("自分で書いたスクリプト")</script>
<script>alert("攻撃者が差し込んだスクリプト")</script>

ブラウザは、ヘッダと同じ値のnonce属性を持つタグだけを実行します。上の例では、1つ目は実行され、2つ目はブロックされます。

攻撃者はページを読み込む前に、そのリクエストの値を知ることができません。値を使い回すと意味がなくなるので、リクエストごとに生成するのが条件です。

ハッシュ

スクリプトの中身から計算したハッシュ値を、ヘッダに書いておく方法です。

Content-Security-Policy: script-src 'sha256-abc123...'

ブラウザはインラインスクリプトの中身のハッシュを計算して、ヘッダの値と一致したときだけ実行します。1文字でも違えば別のハッシュになるので、中身を書き換えられたり、別のスクリプトを差し込まれたりしても通りません。

中身が決まっている静的なスクリプトに向いていて、HTMLを動的に生成できるなら、nonceのほうが扱いやすいです。

フレームワークが勝手にインラインスクリプトを吐く場合は、ここで詰まりやすいところです。

いきなり本番でブロックすると画面が壊れるので、まずContent-Security-Policy-Report-Onlyで違反をレポートだけさせて、何が引っかかるかを見るのが定石です。

X-Content-Type-Options

X-Content-Type-Options: nosniff

これは設定できる値がnosniffの1つだけです。

ブラウザは、サーバーが返したContent-Typeが怪しいとき、中身を見て種類を推測する(スニッフィングする)ことがあります。たとえばユーザーがアップロードした画像として配信されているファイルの中身が実はJavaScriptだと、ブラウザが推測してスクリプトとして実行してしまう、という事故が起こりえます。

nosniffを付けると、ブラウザは宣言されたContent-Typeを信じて、推測しなくなります。スクリプトとして読み込む場所にtext/javascript以外のものが来たらブロックされます。

付けるデメリットはほぼなく、Content-Typeを正しく返していれば何も壊れません。逆にContent-Typeの指定が雑だったサイトでは、これを入れた瞬間に動かなくなる箇所が見つかります。それはそれで有益な発見です。

Referrer-Policy

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

ブラウザはリンクを辿るとき、遷移元のURLをRefererヘッダで遷移先に伝えます。

困るのは、URLにパスやクエリが含まれている場合です。たとえばパスワードリセット用のトークンがURLに入っているページから外部サイトへのリンクを踏むと、そのトークン付きURLが外部サイトのログに残りえます。

Referrer-Policyで、Refererにどこまで含めるかを指定します。指定できる値は8種類あります。

値 送られるReferer
no-referrer 一切送らない
same-origin 同じオリジンのときだけフルURLを送る
origin 常にオリジンだけ
strict-origin 常にオリジンだけ。ただしHTTPSからHTTPへの降格では送らない
origin-when-cross-origin 同じオリジンならフルURL、別オリジンならオリジンだけ
strict-origin-when-cross-origin 上に加えて、HTTPSからHTTPへの降格では送らない
no-referrer-when-downgrade 降格以外はフルURLを送る
unsafe-url 常にフルURLを送る

下に行くほど緩く、unsafe-urlは外部サイトにもクエリ付きのURLがそのまま渡ります。

このうち、例に挙げたstrict-origin-when-cross-originの挙動は次の通りです。

遷移先 送られるReferer
同じオリジン フルURL(パス・クエリ込み)
別オリジン (HTTPS→HTTPS) オリジンのみ(https://example.com/)
HTTPSからHTTPへの降格 送らない

最近のブラウザはこれがデフォルトの挙動になっています。ただ、ヘッダで明示しておけばブラウザの実装差に左右されませんし、意図が設定として残ります。デフォルトでたまたま安全より明示して安全のほうが、後から読んだ人が助かります。

設定する場所

どれもレスポンスヘッダなので、設定場所はリバースプロキシでもアプリケーションでも構いません。たとえばNext.jsなら、next.config.tsのheaders()で全ページに付けられます。

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          { key: "Strict-Transport-Security", value: "max-age=31536000" },
          { key: "X-Content-Type-Options", value: "nosniff" },
          { key: "X-Frame-Options", value: "DENY" },
          { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
        ],
      },
    ];
  },
};

CSPだけは、先ほど書いた通りインラインスクリプトの扱いが絡むので、上の4つと同じ気軽さでは入れられません。

付いたかどうかはcurl -I https://example.comでレスポンスヘッダを見れば確認できます。

順番をつけるなら

全部を一気に入れる必要はないと思っています。

  1. X-Content-Type-Options、Referrer-Policy、X-Frame-Optionsは影響が小さいので、まず入れる
  2. HSTSは短いmax-ageから始めて、様子を見ながら延ばす
  3. CSPはReport-Onlyで違反を洗い出してから、少しずつ厳しくする

CSPの運用は自分もまだ手探りな部分が多いので、うまい進め方をご存じの方がいれば教えてもらえると嬉しいです。

参照

  1. HTTPSへのリダイレクトを攻撃者が中継して握りつぶし、ユーザーを平文のHTTP通信に留め置く攻撃。 ↩

  2. 正規サイトのページを透明なiframeで重ね、ユーザーに意図しない操作をさせる攻撃。 ↩

  3. ブラウザがContent-Typeを信用せず、レスポンスの中身から種類を推測して扱うこと。 ↩

  4. Cross-Site Scripting。攻撃者が用意したスクリプトを、正規サイトのページ上で実行させる攻撃。 ↩

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?