この記事は エニプラ Advent Calendar 2020 の 11 日目の記事です。
はじめに
Express 公式の『実稼働環境における Express のセキュリティーに関するベスト・プラクティス』というドキュメントでは、「脆弱性対策のために Helmet を使ってね(要約)」と書いているけれど、どんなリクエストヘッダが設定されて、それによってどんな効果があるのか具体的に把握していなかったので、勉強がてらに調べてみました。
私自身セキュリティには勉強中の身であり、ことセキュリティに関しては内容の正確性が求められると思いますので、誤りや勘違いなどがございましたらどうぞご指摘頂ければ幸いです
Helmet とは?👷
Helmet は Express で作成された Web アプリケーションに対して HTTP ヘッダを設定することで、webアプリケーションをセキュリティ的に堅牢化するライブラリです。
(Express に限らず Koa 用のラッパーもあるようです)
ただ、公式で
It's not a silver bullet, but it can help!
とあるように、必ずしもセキュリティ的な問題を一挙に解決してくれる万能薬ではありません。
実際、おそらく CSRF やセッションハイジャック、SQL インジェクションなどへの対策としては有効でないと思われますが、XSS やクリックジャッキングなどの対策にはなると思われます。
Helmet を実際に試してみる
それでは、さっそく Helmet を使ってみましょう。
環境は以下の通りです。
Node.js | npm | Express | Helmet |
---|---|---|---|
v14.15.0 | 6.14.8 | 4.17.1 | 4.2.0 |
Express と Helmet をインストール
新しいフォルダを作り、Express と Helmet をインストールします。
> mkdir express-helmet-sample
> cd express-helmet-sample
> npm init
> npm install --save express helmet
Helmet を使う前のレスポンスヘッダを見てみる
新しく作ったフォルダ内に、app.ts
ファイルを作成し、公式を参考にコードを記述します。
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log('listening on port:', port)
})
サーバーを起動して、ポート3000
で接続を待ち受けるコードです。
ルートURL(/
)に対して HTTP GET リクエストを投げてみます。
app.js
を実行してサーバを起動した後、
> node app.js
--dump-header
付きの curl
コマンドを実行し、起動したサーバのルート URL から HTTP GET レスポンスヘッダを取得します。
> curl --dump-header - http://localhost:3000
取得したレスポンスヘッダは以下の通りです。
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 12
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
Date: Fri, 13 Nov 2020 15:05:20 GMT
Connection: keep-alive
Keep-Alive: timeout=5
特に何の変哲もなさそうですが、X-Powered-By
ヘッダを見ると、このレスポンスが Express によって作られたアプリから返されていることがが分かります。この場合、攻撃者はサーバが Express によって起動されていると分かるので、攻撃前のひと手間が省かれてしまうという懸念になります。
ただ、単に Express だと分かったところで何ができるかは微妙なところではないかな?と思ってしまいますが……。
しかし、MDN でも以下のように注意書きがなされているため、X-Powered-By
ヘッダは無くしたほうが無難だと思われます。
ホスティング環境やその他のフレームワークによって設定される可能性があり、アプリケーションや訪問者に有益ではない情報を含みます。潜在的な脆弱性が発現することを防ぐために、このヘッダーは設定しないでください。
ここでは、単に Express サーバを起動しただけでは、X-Powered-By
のようなリスクのあるヘッダがそのままレスポンスに含まれてしまうことに注意してみます。
Helmet を被ってみる
ここでヘルメットを使用してみます。
以下のようにコードを変更します。
const express = require('express')
const helmet = require('helmet')
const app = express()
const port = 3000
app.use(helmet()) // ヘルメットを使用する
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log('listening on port', port)
})
サーバを再起動し、改めて curl
コマンドによりレスポンスヘッダを取得します。
> node app.js
> curl --dump-header - http://localhost:3000
レスポンスヘッダの中身を見てみると、先ほどとは様変わりしていることが伺えます。
先ほど言及した X-Powered-By
が消えていますね。
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
X-DNS-Prefetch-Control: off
Expect-CT: max-age=0
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Download-Options: noopen
X-Content-Type-Options: nosniff
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: no-referrer
X-XSS-Protection: 0
Content-Type: text/html; charset=utf-8
Content-Length: 12
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
Date: Fri, 13 Nov 2020 15:08:04 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Hello World!
以上の新たに Helmet によって付加されたヘッダはセキュリティに関するヘッダであり、これらをレスポンスに設定することで様々な恩恵が得られます。
Helmet で対策される脆弱性
Helmet で追加されたヘッダーとそれにより対策される脆弱性を一覧化すると、次のようになります。
(ただし、必ずしもすべての対策がデフォルトで設定されるわけではありません)
ヘッダー | 対策される脆弱性 | Helmet API |
---|---|---|
Content-Security-Policy1 | XSS攻撃 | helmet.contentSecurityPolicy(options) |
X-DNS-Prefetch-Control2 | 情報漏洩 | helmet.dnsPrefetchControl(options) |
Expect-CT3 | 不正なSSL証明書 | helmet.expectCt(options) |
X-Frame-Options4 | クリックジャッキング攻撃 | helmet.frameguard(options) |
Strict-Transport-Security5 | 中間者攻撃 | helmet.hsts(options) |
X-Download-Options | フィッシング | helmet.ieNoOpen() |
X-Content-Type-Options6 | MIME スニッフィング | helmet.noSniff() |
X-Permitted-Cross-Domain-Policies7 | XSS攻撃 | helmet.permittedCrossDomainPolicies(options) |
Referrer-Policy8 | 情報漏洩 | helmet.referrerPolicy(options) |
X-XSS-Protection9 | XSS攻撃 | helmet.xssFilter() |
一つ一つ簡単に見ていきたいと思います。
Content-Security-Policy (CSP)
Content-Security-Policy: default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self'
https: 'unsafe-inline';upgrade-insecure-requests
これは主に XSS 攻撃を緩和する役目を果たします。
実行を許可するコンテンツのホワイトリストを作成し、その参照元のリソースだけを実行及びレンダリングするようにブラウザに指示します。
これにより、信頼できないコンテンツを事前に弾くことができ、不正なコードが注入されることを防ぎます。
Content-Security-Policy
ではホワイトリストの一行一行をポリシーとして定義し、以下のようにセミコロン区切りで一つ一つ設定することになっています。
Content-Security-Policy: *policy*; *policy*; ...;
ポリシーについて一つ一つ見ていくと脱線するので、ここでは省略します。
X-DNS-Prefetch-Control
X-DNS-Prefetch-Control: off
ブラウザが事前にドメイン名の解決を実行する機能を制御します。ここでは DNS 先読みを無効にしています。MDN2には、
これはページのリンクを制御しない場合や、ドメインに情報漏洩させたくないと分かる場合に有用です。
とあり、Helmet の README でも
helmet.dnsPrefetchControl sets the X-DNS-Prefetch-Control header to help control DNS prefetching, which can improve user privacy at the expense of performance. See documentation on MDN for more.
とあるので、パフォーマンスを犠牲にしてでもユーザーのプライバシーを守るために off
としているようですが、正直なぜ??という感じでいまいち理解できなかったのでもう少し調べてみました。
すると、同じような疑問を抱いていた方がほかにもいました。
私の理解の及ぶ限りで要約すると、DNS 先読みが有効であると、そのページで参照される全ての外部リソースに対してホスト解決のための DNS リクエストが発行されてしまい、それらが第三者によって盗聴される可能性があることを含めると、むやみにホスト解決を行うことは情報漏洩を招くことになる……ということだと思います。
Expect-CT
Expect-CT: max-age=0
不正な SSL 証明書を検知するための仕組みです。
以下の記事がお詳しいです。
デフォルトでは max-age
が 0
になっており、特に何も設定していません。
ユーザが意図的に有効にする必要があるようです。
X-Frame-Options
X-Frame-Options: SAMEORIGIN
ブラウザがページを <frame>
, <iframe>
, <embed>
, <object>
の中に表示することを許可するか示すために使用されます。
CSP の frame-ancestors
に似ており、実際 CSP で代替できますが、古いブラウザ向けにまだ有効のようです。
ここでは SAMEORIGIN
を指定することで、ページ自体と同じオリジンのフレーム内でのみ表示できるようになります。
Strict-Transport-Security
Strict-Transport-Security: max-age=15552000; includeSubDomains
ウェブサイトがブラウザに対して HTTP の代わりに HTTPS を用いて通信を行うように指示します。
MDN5 の以下の解説がとても分かりやすいです。
もし、訪問者が http://www.foo.com/ または単に foo.com と入力したとき、ウェブサイトが接続を HTTP で受け付け、 HTTPS にリダイレクトするようになっていると、訪問者はリダイレクトされる前にまず、暗号化されないバージョンのサイトと通信する可能性があります。これは中間者攻撃の機会を作ってしまいます。リダイレクトは訪問者を、本来のサイトの安全なバージョンではなく、悪意のあるサイトに導くために利用される可能性があるからです。
HTTP の Strict Transport Security ヘッダーは、ブラウザーに対してサイトを HTTP を使用して読み込まず、サイトへのすべてのアクセスを、自動的に HTTP から HTTPS リクエストに変換するよう指示することができます。
ヘッダと直接関係はありませんが、リダイレクト自体、オープンリダイレクト脆弱性などを引き起こす可能性があるので注意したほうがよさそうです。
X-Download-Options
X-Download-Options: noopen
ブラウザ(IE8)がアプリケーションからの安全でない可能性のあるダウンロードを必ず保存させるようにします。
これにより、サイトのコンテキストで HTML の実行を抑止します。
X-Content-Type-Options
X-Content-Type-Options: nosniff
Content-Type
ヘッダーで示された MIME タイプを変更せずに従うようにします。
これは MIME スニッフィング対策に有効です。
X-Permitted-Cross-Domain-Policies
X-Permitted-Cross-Domain-Policies: none
Adobe 製品に特有のクロスドメインポリシーファイル(crossdomain.xml
)を許可するかどうかを指定します。
crossdomain.xml
は同一ドメインポリシーによって制限されているドメイン間のデータを処理する許可を与えるポリシーを定義することができ、none
に設定することですべてのポリシーファイルを不許可にしています。
Referrer-Policy
Referrer-Policy: no-referrer
リファラ情報をリクエストに含めないように設定しています。
これは情報漏洩を未然に防ぐのに有効でもあり、例えば URL にセッション ID などを埋め込んでいて悪意のある外部サイトへのリンクを踏んでしまうと、外部サイトへセッション ID が流出してしまい、セッションハイジャックを引き起こしてしまう可能性があります(徳丸本参照)。
X-XSS-Protection
X-XSS-Protection: 0
X-XSS-Protection による XSS フィルタリングを有効にすると、XSS 攻撃を検出したときにページの読み込みを停止します。
ここでは0
にして XSS フィルタリングを無効化していますが、無効化にしている理由は以下の議論が参考になります。
ちゃんと理解できているか自信ないですが、X-XSS-Protection
による XSS フィルタを利用することで、むしろ悪用されて XS-Leak 攻撃を引き起こす危険性があるため、無効にした方がいいらしい?
まとめ
今回調べてみて、DNS 先読みや Certificate Transparency、X-XSS-Protection
の悪用については全然知らなかったので、勉強になりました。
ただ、今回は表層を撫でてみただけで、「完全に理解した」には至っていないので、今回調べたことをもとに各種ヘッダーや Web の脆弱性そのものについてそれぞれ更に深く調べていきたいと思います。
余談
余談になりますが、先日ウェブ・セキュリティ試験(徳丸基礎試験)を受験して、無事に合格することができました。
この試験では今回見たようなウェブ・セキュリティの基礎的な知見を問うもので、とてもよい試験でした。試験については以下の記事がとても参考になります。
恥ずかしながら、今まで漠然とした不安の中で Web アプリケーションを開発していたのですが、この試験のために『体系的に学ぶ 安全なWebアプリケーションの作り方』(通称徳丸本)を何度か読み直して勉強したことで、少なからぬ根拠を持って実装に当たれるようになりました。
昨今ウェブ・セキュリティの需要が高まっている(と勝手に思っています)中、皆さんも年末年始楽しんで勉強してみてはいかがでしょうか。
出典
- Helmet
- 実稼働環境における Express のセキュリティーに関するベスト・プラクティス
- HTTP ヘッダー - HTTP | MDN
- コンテンツ セキュリティ ポリシー | Web | Google Developers
- web browser - What security implications does DNS prefetching have? - Information Security Stack Exchange
- Certificate Transparency - Web security | MDN
- CT対応を示すExpect-CTヘッダとは - ASnoKaze blog