ExpressでHelmet(v6.0.1)を使うと付与されるHTTPヘッダ
Node.jsでWebアプリケーション作成されるExpressの公式ガイドラインでは、セキュリティ対策としてHelmetの利用が推奨されています。
Helmet can help protect your app from some well-known web vulnerabilities by setting HTTP headers appropriately.
Helmet is a collection of several smaller middleware functions that set security-related HTTP response headers.
このHelmetをExpressのミドルウェアとして使うと、セキュリティ関連の必要なHTTPレスポンスヘッダを設定してくれることでWebアプリケーションを保護できるというものです。
注意点として、いくつかのヘッダ(例えばContent-Security-PolicyやCross-Origin関連)をそのまま使うと外部リソースの読み込みができなくなったり、インラインのJavaScript(HTMLファイルのscriptタグに直接書いたJS)が動かなくなったりすることがあります。
上と同じような現象になったことがあるので、今度サンプルコードを書いて動作を見てみたいと思っています。
今回の記事ではHelmetはどのようなHTTPヘッダをセットしてくれるかを確認し、それぞれのヘッダの役割について勉強していきます。
同じようなことを試している記事がQiitaにすでに存在していますが、Helmetのバージョンがv4.2.0となっています。
今回はv6.0.1を使っており、セットされるHTTPヘッダに違いがありました。
- Helmet v4.2.0のときにセットされるHTTPヘッダの情報: Express×Helmetでウェブセキュリティを学ぶ - Qiita
環境・バージョン
バージョン | |
---|---|
Node.js | 16.19.1 |
npm | 8.19.3 |
Express | 4.18.2 |
Helmet | 6.0.1 |
Google Chrome | 111.0.5563.148 |
const express = require('express');
const helmet = require('helmet');
const app = express();
app.use(helmet());
app.get('/', (req, res) => {
res.status(200).send('use helmet');
});
app.listen(3000);
HelmetがセットするHTTPレスポンスヘッダ一覧
まずはHelmetなしのときに設定されるHTTPレスポンスヘッダを確認します。
X-Powered-By: Express
ETag: W/"e-34iY2aJdh23Y4/jkZycmy35iyiA"
Date: Thr, 01 Apr 2023 12:00:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
次にHelmetをデフォルトで利用したときに設定されるHTTPレスポンスヘッダです。
(ちなみにこのデフォルトで設定されるヘッダ情報はHelmet公式サイトにちゃんと書かれていました。)
Content-Security-Policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';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
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
X-DNS-Prefetch-Control: off
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Download-Options: noopen
X-Content-Type-Options: nosniff
Origin-Agent-Cluster: ?1
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: no-referrer
X-XSS-Protection: 0
ETag: W/"a-jPyRejoCXiFb4CMwF2krB/wtbcM"
Date: Thr, 01 Apr 2023 12:00:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
HTTPレスポンスヘッダ | Helmetなし | Helmetあり |
---|---|---|
X-Powered-By | Express |
(セットされない) |
ETag | (ランダム値) | (ランダム値) |
Date | (日時) | (日時) |
Connection | keep-alive |
keep-alive |
Keep-Alive | timeout=5 |
timeout=5 |
Content-Security-Policy | (セットされない) | default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';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 |
Cross-Origin-Embedder-Policy | (セットされない) | require-corp |
Cross-Origin-Opener-Policy | (セットされない) | same-origin |
Cross-Origin-Resource-Policy | (セットされない) | same-origin |
X-DNS-Prefetch-Control | (セットされない) | off |
X-Frame-Options | (セットされない) | SAMEORIGIN |
Strict-Transport-Security | (セットされない) | max-age=15552000; includeSubDomains |
X-Download-Options | (セットされない) | noopen |
X-Content-Type-Options | (セットされない) | nosniff |
Origin-Agent-Cluster | (セットされない) | ?1 |
X-Permitted-Cross-Domain-Policies | (セットされない) | none |
Referrer-Policy | (セットされない) | no-referrer |
X-XSS-Protection | (セットされない) | 0 |
わかったことを簡単にまとめると、
- X-Powered-ByヘッダはHelmetによって削除される
- ETag、Date、Connection、Keep-Aliveヘッダは変更なし
- その他セキュリティ関連のヘッダがHelmetによって追加でセットされる
また、Helmetによって追加されるHTTPヘッダが前述のQiitaの記事(Helmet v4.2.0)と異なっているということです。HelmetがRFCの変更や昨今のセキュリティ情報に対応しているということでしょう。
このあとはHelmetによって変更があったHTTPヘッダについて、その役割を確認していきます。
Helmetが変更したHTTPヘッダの詳細
X-Powered-By
レスポンスを返しているアプリケーションの種類やバージョンが格納されるHTTPヘッダです。
何が使われているかがわかってしまうため、脆弱性を突かれる可能性があるのでこのヘッダは設定しないようにしたほうがよい、ということです。
Express公式ガイドライン Security Best Practices for Express in Production(実稼働環境におけるベスト・プラクティス: セキュリティー)でも、たとえHelmetを使わないくとも少なくともこのヘッダは無効にすべきと書かれています。
Content-Security-Policy
XSS攻撃を軽減するために利用されるヘッダです。
ヘッダの値には、各リソース(JavaScript、CSS、画像など)を、どこから(自分のドメイン、特定の外部ドメイン、JSならscriptタグなど)読み込むことを許可するのかが記述されています。
例えば、JavaScriptの読み込み元を自分のドメインだけ限れば、悪意のある外部サイトからの悪意のあるJavaScriptによるXSS攻撃を避けることができます。
Helmetが設定している値を見ると、基本的にサイトが公開されているサブドメインを除いたドメインからのリソースを許可するようになっているようです。
値の詳細には、下記のサイトを参考にしてください。
参考: コンテンツセキュリティポリシー (CSP) - HTTP | MDN
Cross-Origin-Embedder-Policy
略称はCOEP。
明示的に許可を与えていない外部オリジンのリソースが読み込まれないように設定できるヘッダです。一つ前のContent-Security-Policyと似ていますね。同じようにXSS攻撃を軽減するためのヘッダでしょう。
Helmetで設定されるrequire-corp
という値は、サイト自身と異なるオリジン(外部オリジン)が明示的な許可をしている場合もしくはサイト自身と同じオリジン(同じスキーム、同じホスト、同じポート)場合を除いてリソースを読み込めないようにします。
外部オリジンが許可を示すには、Cross-Origin Resource Sharing(CORS)ヘッダや後述のCross-Origin-Resource-Policy(CORP)ヘッダを外部オリジンが適切に設定して値を返してくれる必要があります。
デフォルト値であるunsafe-none
の場合、CORSやCORPが設定されてなくとも外部オリジンのリソースを読み込みます。
参考
- Cross-Origin-Embedder-Policy - HTTP | MDN
- Cross-Origin-Embedder-Policyヘッダについて - ASnoKaze blog
- COOPとCOEPの利用によりあなたのウェブサイトを「クロスオリジン分離」にする
Cross-Origin-Opener-Policy
略称はCOOP(読み方はなんでしょうね?コープだった場合、次のCross-Origin-Resource-Policy(CORP)と被ってしまう気がします)。
あるサイト(親ウィンドウ)が外部オリジンのサイトをウィンドウ(子ウィンドウ)として開いたとき、外部オリジンのサイトが親ウィンドウのプロパティを読み取れないようにするためのヘッダです。
このヘッダはCross-Site Leaks(XS-Leaks)攻撃を防止するために使われます。
XS-Leaksとは、HTTPレスポンスのステータスコードや応答時間、ウィンドウ内のiframeの数などからユーザの情報、もしくはその断片を取得する攻撃です。
Helmetによって設定されるsame-origin
という値は、同じオリジンだけが親ウィンドウのプロパティを取得できるようにします。他のオリジンからは許可しません。
デフォルト値であるunsafe-none
は、他のオリジンからも取得できるようになります。
same-origin-allow-popups
は、COOPを設定していないかunsafe-none
を設定して開いた他のオリジンからの取得は許可する(らしい、ちょっと自信がないです)。セキュリティの強さとしてはsame-origin
とunsafe-none
の間です。
参考
- Cross-Origin-Opener-Policy - HTTP | MDN
- Cross-Origin-Opener-Policyについて - ASnoKaze blog
- COOPとCOEPの利用によりあなたのウェブサイトを「クロスオリジン分離」にする
- Blind XS-LeaksとSOP回避の話 - よーでんのブログ
- XS Leaks - OWASP Cheat Sheet Series
- 14種類の XS-Leaks 攻撃:Chrome/Edge/Safari/Firefox などに影響する – IoT OT Security News
Cross-Origin-Resource-Policy
略称はCORP。
ウェブサイトやアプリケーション(がホストしている画像やJavaScript)が他のオリジンから読み込まれることを許可しなくなるヘッダ。
Spectre(スペクター)攻撃やCross-Site Script Inclusion(XSSI)攻撃を防止するために使われます。
Spectre攻撃とはCPUの脆弱性を突く攻撃であり、攻撃者が他オリジンからサイトに不正なリクエストを送ることで攻撃を成立させます。XSSI攻撃とは、攻撃者が用意した他オリジンからサイトのホストしているJavaScriptを呼び出し、そのJavaScriptに機密情報が含まれている場合、その情報が他オリジンに流出するというものです。いずれにせよ、外部のオリジンからのアクセスを遮断することで防御することができ、そのためにCORPヘッダは利用できます。
値がHelmetが設定するsame-origin
の場合、同じオリジン(同じスキーム、同じホスト、同じポート)からの読み込みだけを許可します。
same-site
の場合、同じサイト(サブドメインは異なっていたり、ポートが異なっていても同じサイト)からの読み込みを許可します。
cross-origin
の場合、どこからでも読み込むことができます。
参考
- Cross-Origin-Resource-Policy - HTTP | MDN
- Cross-Origin Resource Policy (CORP) - HTTP | MDN
- Cross-Origin-Resource-Policyヘッダとは - ASnoKaze blog
- Spectre - Wikipedia
- XSSI(Cross-Site Script Inclusion)攻撃とは【セキュリティ】 - まったり技術ブログ
- クロスサイトスクリプトインクルージョン (XSSI) | Tenable®
X-DNS-Prefetch-Control
ブラウザがHTML上のリンク、画像などのドメイン名をバックグラウンドで事前に解決する(DNS先読みと呼ぶ)機能を制御するためのヘッダ。
DNS先読みをおこなうことで、ユーザがリンクをクリックして遷移するときのパフォーマンスが改善されます。
セキュリティリスクとしては、攻撃者が管理するドメインに対するDNSクエリを監視することでどのようなネットワークを利用しているかを盗聴されたり、サブドメイン名(ホスト名)を一意に識別できるものに変更することで追跡されたりすることです。
さらに、JavaScriptによってセッションIDなどのセキュリティ情報をホスト名に埋め込ませ、送られてくるDNSクエリを取得することで情報を盗み出すという手法も考えられます。
Helmetが設定する値であるoff
はDNS先読みを無効にします。
反対にon
はDNS先読みを有効にします。
参考
- X-DNS-Prefetch-Control - HTTP | MDN
- Bypassing Content-Security-Policy with DNS prefetching – Compass Security Blog
- express - How does DNS prefetching endanger security/privacy? - Stack Overflow
- web browser - What security implications does DNS prefetching have? - Information Security Stack Exchange
X-Frame-Options
ページを他のサイトのframeタグ、iframeタグ、embedタグ、objectタグの中に表示することを許可するかどうかを示すヘッダ。指定がない場合、すべてのサイトから利用されることを許可します。前述のContent-Security-Policyヘッダに取って代わられつつあるものの、まだ有効です。
他のサイトに埋め込まれないようにすることでクリックジャッキング攻撃を防ぐことができます。クリックジャッキング攻撃については下の参考に記載したIPAの説明がわかりやすいです(サイト内の解決策としてはもこのヘッダを利用することがかかれています)。
HelmetはSAMEORIGIN
(ほかは小文字で指定することが多いなか、ここは大文字が必須?)という値を設定します。これによってページと同じオリジンのとき、表示を許可します。
DENY
という値もあり、これはすべてのサイトで許可しません。安全性を高めるにはこちらのほうが良さそうですが、使い勝手がわるすぎるのかもしれません。
参考
Strict-Transport-Security
HTTPに対して指示を出すことからHSTSと略されます。
サイトがブラウザにHTTPの代わりにHTTPSを利用すること指示するためのヘッダ。
この設定が有効な場合、サイトにHTTPでアクセスしてもHTTPSにリダイレクトされます。
Chromeではchrome://net-internals/#hsts
にアクセスするとHSTSの設定を確認・追加・削除をおこなうことができます。
以前、localhost
がHSTSによってHTTPSで接続するようになってしまい、その後のアクセスで困ったことがありました。そのときに知った設定です。
さて、Helmetがこのヘッダに設定した値は、max-age=15552000; includeSubDomain
となっています。
まず、max-age
はHTTPSを利用することをブラウザが記憶する時間であり、単位は秒となります。なので、この場合は180日間となっています。次のincludeSubDomain
はHSTSの設定をサブドメインにも適用する、という指示です。
サブドメインに適用しない場合、includeSubDomain
は設定しません。
また、preload
という値を設定することもできます。これは仕様にはなっていないものの、主要なブラウザは仕様しているようです。HTTP接続をしてHTSTの値を見てからHTTPSに切り替えるのではなく、最初からHTTPSで接続をおこなわせる指示です。これには、Googleが提供しているHSTS先読みサービスを利用しており、ドメインを登録することで先読みされるようになります。
参考
X-Download-Options
IEでダウンロードしたファイルを「開く」という動作を実施させないようにするヘッダ。
IEでファイルをダウンロードすると、通常ではダイアログ画面が表示され、「開く(open)」、「保存(save)」、「キャンセル(cancel)」という選択が現れます。「保存」を選ぶとダウンロードフォルダにファイルを保存します。
「開く」を選ぶと、そのままブラウザでファイルを開こうとします。そして、IE上でダウンロードしたファイルを開いてしまうと、インジェクション攻撃を受ける可能性があります。
Helmetが指定しているnoopen
によって、ダイアログにそもそも「開く」という選択肢が表示されなくなります。
参考
X-Content-Type-Options
IEはコンテンツを確認し、Content-Typeヘッダで指定されているMIMEタイプを書き換えることがあり、それをさせなくするためのヘッダです。
プレーンテキストや画像として配信しているはずが実は攻撃者のJavaScriptであり、それが被害者のブラウザで実行されることによってXSS攻撃を受けることがあります。
Helmetはnosniff
を設定しており、これによってMIMEタイプの書き換えを禁止します。
参考
- X-Content-Type-Options - HTTP | MDN
- MIMEスニッフィングを利用した脆弱性とその対策方法 | GMOアドパートナーズ TECH BLOG byGMO
- [無視できない]IEのContent-Type無視:教科書に載らないWebアプリケーションセキュリティ(2)(1/2 ページ) - @IT
Origin-Agent-Cluster
document.domain
を使ってドメイン名をセットすることを許可するかを示すヘッダ。
document.domain
はサイトのドメイン名を取得できるプロパティであり、またドメイン名をセットすることもできます。
ドメイン名をセットすることのメリットは、同一オリジンポリシーを緩和することができ、CORSなどの制限を超えて別ドメインのリソースを利用できるようになります。
Helmetは?1
という値を指定しており、これはtrue
を表します。これにより、document.domain
で値をセットすることができるようになります。Helmetはセキュリティ目的というよりも使い勝手ということかもしれません。(セキュリティ理由があったらごめんなさい。。。)
ヘッダの値としては他に?0
があり、これはfalse
を表します。document.domain
で値をセットすることができなくなります。
参考
- Deprecating document.domain setter
- Eiji Kitamura / えーじさんはTwitterを使っています: 「もうすぐChromeで
document.domain
が変更できなくなります。このテクニックを使ってsame-origin policyを回避してiframeとコミュニケートしているサイトは対応が必要です。 // Chrome will disable modifyingdocument.domain
to relax the same-origin policy https://t.co/XoHQD6rDEM」 / Twitter
X-Permitted-Cross-Domain-Policies
AdobeのAcrobatやFlashではcrossdomain.xml
というファイルを使って別のドメインからリソースを取得することを許可しています。
このヘッダではcrossdomain.xml
というファイルの利用を制限します。
Helmetはnone
を指定しており、これによりすべてのcrossdomain.xml
が不許可となります。
参考
- Cross Domain Configuration — Acrobat Desktop Application Security Guide
- メタポリシーを使った Flash Player セキュリティ管理 - akihiro kamijo
Referrer-Policy
リクエストにRefererヘッダを含めるかを制御するヘッダ。
Refererヘッダにはアクセス元(参照元)のサイト情報が含まれています。
URLに機密情報が含まれている場合、情報漏洩の危険性があり、これを防ぐためにReferrer-Policyヘッダを利用します。
Helmetはno-referrer
を指定しており、Refererヘッダが省略されます。
他にも指定できる値はありますが、省略します。参考に書いてあるMDNのサイトに詳細が書かれています。
参考
X-XSS-Protection
反射型XSS攻撃を検出したとき、ページの読み込みを停止するためのヘッダ。
攻撃者が用意したスクリプトがリクエストに含まれて一度サーバに送られ、スクリプトが含まれたレスポンスが被害者のブラウザに戻ってきて実行されることによって受ける攻撃を反射型XSS攻撃と呼びます。
Content-Security-Policyヘッダが導入され、このヘッダは不要になることから仕様として標準化されることはありません。
いくつかのブラウザではこのヘッダに対応していません。
将来的にはHelmetもこのヘッダを設定しなくなるかもしれないですね。
Helmetでは0
という値を指定していますが、これはXSSフィルタリングを無効にします。
反射型XSS攻撃を防ぐためのXSSフィルタリングですが、この機能に脆弱性があります。
脆弱性もいくつかあるようですが、簡単なものとしてはXSSフィルタが誤検出することで検知箇所をサニタイズし、意図しないJavaScriptが実行されることがあります。
このヘッダに指定できる値は他にもありますが、省略します。詳しくはMDNの解説を確認してください。
- X-XSS-Protection - HTTP | MDN
- X-XSS-Protectionの引退に備えCSP導入を検討しよう - 株式会社セキュアイノベーション
- クロスサイトスクリプティング【Cross-Site Scripting:XSS】とは|図でわかる脆弱性の仕組み | ユービーセキュア
- ブラウザのXSSフィルタを利用した情報窃取攻撃 | MBSD Blog
おわりに
最後のほうのヘッダはだんだん疲れてきて記述が雑になってしまいました。
この記事ではHelmet v6.0.1を使って検証しましたが、この段落を書いている時点(2023/05/04)でv6.1.5になっていました。
先駆者のQiita記事(Helmet v4.2.0)と異なる部分もあり、セキュリティに関する情報はどんどん変わっていっているということでしょうか。自分の勉強不足も多く、反省するばかりです。
メジャーバージョンアップの際には再確認したほうが良さそうです。
Discussion