エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

セキュリティヘッダ警察です!既に包囲されている!観念してヘッダを挿入しなさい!

【セキュリティチームブログリレー2回目】

こんにちは。エンジニアリンググループの山本です。

セキュリティチームは、エンジニアリンググループ全体のセキュリティを向上させるためのバーチャルチームなのですが、各プロダクト開発チームのサービスをチェックして、協力しながら全体のセキュリティを向上させていくのがミッションです。 そのお仕事の一環として「この部分、セキュリティヘッダが足りないから入れてください!」というやりとりを日常的に行なっています。

今日はこの「セキュリティヘッダ」というものが一体何なのか、今さら人に聞けないアレコレを取りまとめてみたいと思います。

セキュリティヘッダ警察の日常の図(もちろん冗談です)

セキュリティヘッダ

HTTPのセキュリティヘッダと呼ばれるものはいくつもあります。それを入れたらなにが嬉しいのか、なにが嬉しくないのか、危険なのかそうでないのか。 「よくわかんないけど入れてみました!」という形でも問題ないことはありますが、新規のシステムに入れるならまだしも、既存システムに入れていくとなるとそれなりに調査が必要となることも多いと思います。

今回はセキュリティヘッダと呼ばれるものについて取りまとめてみたいと思います。

そもそもセキュリティヘッダとは?

セキュリティヘッダと呼ばれるものはHTTPのレスポンスヘッダのうち一連のものであり、それを入れておくことによってブラウザに対して特定の動きを制限するように要請するものです。 各種存在するのですが、Strict-Transport-Security のように RFCで規格化されているものもあれば、X-Content-Type-Options のようにX付きの非標準ヘッダもあります。(X付きのヘッダはRFC 6648で非推奨となっているのですが、現実として使われているのでここでも挙げていきます。)

比較的安全なセキュリティヘッダ

X-Content-Type-Options

X-Content-Type-Options: nosniff

上記のように記述いたします。ブラウザはMIME スニッフィングと呼ばれる手法でコンテンツの形式(Content-Type)を推定しようとします。仮にContent-Type: text/html と書いてあっても、中身がそうとは思えない時には image/jpeg であると判断するというような形です。 nosniff はそのような推測を許可しないことをブラウザに伝えます。

これによってなにが防げるのでしょう。 例えばユーザが画像をアップロードするサービスにおいて、悪意あるJavaScriptが仕込まれたHTMLを「画像として」アップロードするとします。アップロードされた「画像(実はスクリプト入りのJavaScipt)」があるURLを他人に踏ませることで、当該JavaScriptはそのサービスを行なっているサイトのJavaScriptとして実行されることになります。スニッフィングを禁止すれば画像と解釈されるためXSSを防御できるのです。

普通に考えて、Content-Type 通りに解釈してくれることは正しい挙動であるはずなのでnosniffを付与することの弊害はほぼありません。少なくとも私は耳にしたことがありません。

追記:上記記述に関して、Twitterで Content-Type について「たとえば、JSONPなのにtext/htmlやapplication/jsonを返していたら動かなくなります」とのご指摘(https://twitter.com/ockeghem/status/1668489591157063681)をいただきました。Content-Type として現状で「正しい」ものを返しているつもりでも、実はMIMEスニッフィングに頼っていて実際には異なった解釈で動作させているという場合も存在するかもしれませんので、注意が必要であるようです。もうちょっと頑張ります!

X-XSS-Protection

このヘッダは仮に使うとすれば次のように使います。

X-XSS-Protection: 1; mode=block

これをみたブラウザは自身の中にもつXSSフィルタ(XSSと思われるパターンを検知したときにページの処理を停止する)を有効化することが求められます。

しかし…。実はこの機能はもはや時代遅れとなってしまいました。既にサポートされていないのです。 新規に入れる必要はありませんし、場合によっては有害になる場合さえあるようです(下記リンク先参照)ので既に入っている場合は機会を見て外していくとよさそうです。

X-XSS-Protection - HTTP | MDN

XSS対策としては既に意味をなしていないため、後述の Content-Security-Policyヘッダを活用することが求められます。

Strict-Transport-Security

これはいわゆる「HSTS」と呼ばれるものです。

下記のように使います。

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

意識の高い皆様は今どきHTTP(not HTTPS)を使っているなどということはないと思うのですが、古いシステムでどこやらに残っているということがあるかもしれません。それでも普通はHTTPSでもサービスを提供しているとは思いますし、HTTPアクセスが来たら301でHTTPSにリダイレクトさせているのではないかと思います。

HSTS(HTTP Strict Transport Security) は HTTPでアクセスしようとしても、ブラウザ側で勝手にHTTPSに飛ばしてしまうことを要求するという機能です。 max-age は覚えておく秒数、includeSubDomains は、例えば https://example.com/ でこのヘッダを見かけた場合、 http://sub.example.com/ のようなサブドメインアクセスも含めて、 https://sub.example.com/ へと置き換えることを要求するものです。

例えばですが、https://example.com/ で素敵なサービスを提供していたとします。その時、(偽のフリーWi-FiなどでDNSが乗っ取られているなどして)悪意のある偽物の http://example.com/ に誘導することはあり得ます。そこから本物の https://example.com/ にproxyしてしまえば、いわゆる中間者攻撃が成立します。

この時でも、https://example.com/ で一度でもHSTSヘッダを読み込んでおけばブラウザが https://example.com/ に勝手にリダイレクトしてくれるため、証明書の検証がなされることでそのような中間者攻撃を防げるのです。

どうしてもHTTPを使いたいという事例などはほぼ消滅していると思いますのでそれほど懸念なく導入可能です。また、このヘッダはHTTPで送信するものではありません。HTTPSで送信してください。

少し調査が必要なセキュリティヘッダ

X-Frame-Options

下記のどちらかを使います。

X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN

<frame> <iframe> <embed> などのソースとしてなにを許すのかという話です。DENYは完全不許可、 SAMEORIGINはいわゆる「セイムオリジン」です。つまり同じホストで同じスキーム(https/http)で同じポートであれば許可するということです。

このヘッダを付与しておくことで、自サイトのコンテンツが他サイトに埋め込まれないよう保証することができます。

この X-Frame-Options も非推奨なのですが、これを代替するものが Content-Security-Policyであり、少し難易度が上がるため X-Frame-Options を入れてしまうこと自体は問題ないと考えています。

追記:こちら当初の説明が誤っていたようですのでTwitterでのご指摘(https://twitter.com/bakera/status/1668448460113321984)を受けて修正いたしました。失礼いたしました。(当初、勘違いして「自サイトに埋め込まれることを防ぐ」のような説明を書いておりましたが、「他サイトに自サイトが埋め込まれるのを防ぐ」というのが正解でした。以下のリンク先も参照。)

X-Frame-Options - HTTP | MDN

よく考えてから導入したいセキュリティヘッダ

Content-Security-Policy

CSPと呼ばれるものです。

以下のような複雑な構造を取ります。

Content-Security-Policy: ディレクティブ1 ソース1 ソース2...; ディレクティブ2 ソース1 ソース2...;

ディレクティブは例えば default-src や script-src、frame-ancestors といったものです。ソースは例えば self や https://* https://example.com/ といったものです。例えばスクリプトの場合ならば <script src="..."> の src として記述可能なドメインを制限したいという時に利用いたします。

詳細はとても書ききれないので以下を参照してください。

developer.mozilla.org

これは多くの場合はXSSのリスクを軽減させるためのものとなります。必要なドメインからしか必要なコンテンツを読み込まないということですが、特に default-src 'self'; とだけ書けば全ての要素についてsame-originからのみの読み込みを許可するということになりますので、比較的安全です。また default-src はなんらかの値(ex. 'self')を入れておいてください。そうしないと、指定されなかった要素については許可がなされてしまいます。

ただ、例えば画像は別ドメインに置いてある場合などもありますので、それについては別途 img-src: 'self' https://image.example.com/; のように書いて上書きする必要があるかもしれません。また、画像については * で全体を許可しようとしても、ネットワークスキーム(ex. https, wss)しか許可されないようです。必要ならば blob: などを明示的に許可する必要があります。

設定していく上で特に問題になりやすいのがインラインスクリプトです。script-src: 'self' 'unsafe-inline' と書けばインラインのスクリプトは許可されます。しかしこれは脆弱な設定でもあるので悩みどころです。これを回避するためには、nonceと呼ばれる値を<script nonce="値">のようにタグに設定しておき、script-src: 'nonce-値'; のような記述をしておいて特定のインラインスクリプトだけを許可するという形をとる必要があります。同様ですがsha256などでハッシュをつけて特定のスクリプトだけを許可することができます。

CSP: script-src - HTTP | MDN

以下のサイトによれば、script-src でホワイトリストに入れたドメインからバイパスしてスクリプトを実行させるする攻撃もあるようです。ですので、可能な限りホワイトリストも廃止して安全だと確認したスクリプトだけを nonce などで指定していくのが好ましいと言えそうです。

Content Security Policy (CSP) Bypass - HackTricks

また、Google アナリティクスその他、外部のツールを使っている場合もあることでしょう。そのような場合になにも考えずにCSPを設定してしまうとそれらのツールは全て停止してしまいます。

このように、Content-Security-Policy は非常に強力で細かなこともできる一方で、きちんと前準備を踏んだのちに導入する必要があるということを認識する必要があります。

Cookieヘッダ

Cookieヘッダは以下の形を取ります。

Set-Cookie: <cookie-name>=<cookie-value> Expires=日付; 属性群

Expireの有無

Expireは言うまでもなくCookieの持続期間です。これは省略できて、その場合はセッションCookieとしてブラウザ終了によって破棄されることとなります。指定した場合はパーシステントCookieとしてブラウザ終了後も残ります。

ログインの鍵となるCookieなど、あまりにも長い期間保持させることは危険であるという警告を受けることもありますが利便性の問題もあるため用途に合わせて決めることとなります。また、例えば重要なシステムのセッション情報などはブラウザでのCookie保持と関係なく例えば30分間操作がなかったら破棄するようなサーバ側での設定があるかもしれませんので、その辺りとの兼ね合いともなります。

Secure属性

最近のシステムならば常につけておくとよさそうです。HTTPSの時のみCookieの送信するという設定です。大抵の場合なにも考えなくともHTTPからHTTPSにリダイレクトさせますが、HTTPのサイトは偽物である可能性を疑う必要がありますのでCookieを詐取されたくないわけです。

HSTSの設定を入れておけばHTTPにはアクセスさえこないのですが、だとしても入れておいて損をすることはありません。

HttpOnly属性

ちょっと名前が混乱を招くような気もする属性ですが、簡単に言えばスクリプトからのCookieアクセスを禁止するという設定です。document.cookie によってJavaScriptからアクセスできないという言い方をしてもよさそうです。

XSSによるJavaScriptからのセッション情報盗み出しなどに強くなるという意味でこれも基本的には設定しておく必要がありますが、たまにJavaScriptからCookieを利用しているような例を見かけますので、テストしてから導入する必要があります。

SameSite属性

CSRF対策となります。 ご存知の方も多いとは思いますが、CSRFとは次のような攻撃です。

  • サイトAに正規にログインして正規にログインCookieを取得
  • 悪意のあるサイトBを閲覧
  • サイトBはサイトAへの悪意あるリンクを踏ませる
  • サイトAに正規にログインしているので、サイトAへのログイン状態で何らかのよくない動作が行われる(ex. mixiの日記に対して「ぼくはまちちゃん」と投稿する)

SameSiteとは「外部サイトからの遷移があった際のアクセスにおいて」Cookieを読み出せるかどうかを制御する属性です。つまり上の例ならばサイトBからサイトAへの遷移においてサイトAに勝手にログインできるかどうかをこの属性によって決めることができます。

典型的なCSRF

設定できる値は以下の3つです。

  • SameSite=None
  • SameSite=Lax (Chrome系のデフォルト)
  • SameSite=Strict

None は外部サイトからの遷移でもCookieを読み出させます。Lax は GETリクエストの時のみ読み出させます。最後に Strictは常に読み出させません。

例えばYahoo! JapanのセッションCookieが Strict だったとします。 もしそうならば外部からYahoo!のサイトへのURLリンク(つまり、GETリクエストでのリンク)が貼ってあったとしても、Strict のせいで毎回ログアウト状態になってしまいます。これは不便ですね。

先ほど確かめてみましたが、実際は Laxでしたし、ちゃんとログイン状態になりました。しかし、私が例えば勝手にYahoo!で投稿するようなPOSTのフォームを外部サイトに作ってもCookieは送付されずログイン拒否されるのです。(もちろん、CSRF対策はCookieのみではありませんのでそれ以外でも防御されているでしょう。None だったら必ずCSRFに脆弱であるとかそういう意味ではありません。)

SameSite=Laxの場合の挙動

また、SiteSite内での遷移には影響しないのですが、これは SameOriginとは少し意味が異なります。SameOriginは同じFQDN/スキーム/ポートである必要がありますが、SameSite の場合は、同じ「サイト」であれば同じです。ブラウザによって異なる部分もありますが、https://a.example.com/ と https://b.example.com/ https://example.com/ https://x.y.example.com/ などはサブドメインが異なっても SameSite となります。

多くの場合は Lax で問題ないと思われますが、厳密なサービスを提供したい場合は Strict も検討してみてください。また、念のためですがこれはCSRF対策の一部でしかありません。SameSite属性を入れたから対策完了という話ではないため、CSRFトークンなどで万全の対策を取ってください。

観念してヘッダを入れましょう

XSS、中間者攻撃、クリックジャッキング、CSRF対策その他の攻撃が、たかがヘッダ1つで緩和できるならば非常に楽です。 新規サービスでは常識として入れていきたいですし、既存サービスも一つ一つ指摘して追加していってもらっているところです。(Content-Security-Policyは少し難航しています。)

ただ、しつこいようですが「ヘッダを入れたから大丈夫です」という話ではありません。各種対策の一つとして実施していきましょう!

We are Hiring!

エムスリーではセキュリティに興味のあるエンジニアも求めております。

開発チームを包囲してビシビシ取締で切符を切っていく仕事ではないのですが、このような分野にも興味あれば是非お声がけください!

open.talentio.com open.talentio.com jobs.m3.com