⛑️

ExpressでHelmet(v6.0.1)を使うと付与されるHTTPヘッダ

2023/05/04に公開

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ヘッダに違いがありました。

環境・バージョン

バージョン
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-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-originunsafe-noneの間です。

参考

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の場合、どこからでも読み込むことができます。

参考

X-DNS-Prefetch-Control

ブラウザがHTML上のリンク、画像などのドメイン名をバックグラウンドで事前に解決する(DNS先読みと呼ぶ)機能を制御するためのヘッダ。
DNS先読みをおこなうことで、ユーザがリンクをクリックして遷移するときのパフォーマンスが改善されます。

セキュリティリスクとしては、攻撃者が管理するドメインに対するDNSクエリを監視することでどのようなネットワークを利用しているかを盗聴されたり、サブドメイン名(ホスト名)を一意に識別できるものに変更することで追跡されたりすることです。
さらに、JavaScriptによってセッションIDなどのセキュリティ情報をホスト名に埋め込ませ、送られてくるDNSクエリを取得することで情報を盗み出すという手法も考えられます。

Helmetが設定する値であるoffはDNS先読みを無効にします。

反対にonはDNS先読みを有効にします。

参考

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タイプの書き換えを禁止します。

参考

Origin-Agent-Cluster

document.domainを使ってドメイン名をセットすることを許可するかを示すヘッダ。

document.domainはサイトのドメイン名を取得できるプロパティであり、またドメイン名をセットすることもできます。
ドメイン名をセットすることのメリットは、同一オリジンポリシーを緩和することができ、CORSなどの制限を超えて別ドメインのリソースを利用できるようになります。

Helmetは?1という値を指定しており、これはtrueを表します。これにより、document.domainで値をセットすることができるようになります。Helmetはセキュリティ目的というよりも使い勝手ということかもしれません。(セキュリティ理由があったらごめんなさい。。。)

ヘッダの値としては他に?0があり、これはfalseを表します。document.domainで値をセットすることができなくなります。

参考

X-Permitted-Cross-Domain-Policies

AdobeのAcrobatやFlashではcrossdomain.xmlというファイルを使って別のドメインからリソースを取得することを許可しています。
このヘッダではcrossdomain.xmlというファイルの利用を制限します。

Helmetはnoneを指定しており、これによりすべてのcrossdomain.xmlが不許可となります。

参考

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の解説を確認してください。

おわりに

最後のほうのヘッダはだんだん疲れてきて記述が雑になってしまいました。

この記事ではHelmet v6.0.1を使って検証しましたが、この段落を書いている時点(2023/05/04)でv6.1.5になっていました。

先駆者のQiita記事(Helmet v4.2.0)と異なる部分もあり、セキュリティに関する情報はどんどん変わっていっているということでしょうか。自分の勉強不足も多く、反省するばかりです。

メジャーバージョンアップの際には再確認したほうが良さそうです。

GitHubで編集を提案

Discussion