Skip to main content

Node.jsサーバーではHTTP Strict Transport Security(HSTS)ヘッダーを使いましょう

著者

Raphael Mun

feature global security

2022年12月29日

0 分で読めます

多くのWebサイトやアプリでは、セキュリティ関連のHTTPヘッダーを使用することが一般的になっています。WebサイトはHTTPリクエストとレスポンスの一部としてヘッダーを使い、HTTPプロトコルで送信されるページやデータに関する情報を伝えます。たとえば、コンテンツが圧縮されたzipファイルであることを示すContent-Encodingヘッダーや、リダイレクト先のURLを指定するLocationヘッダーなどがあります。

現代のWebブラウザーがユーザーを保護するために使用する、数多くのセキュリティヘッダーの中でも、重要なものの1つがHTTP Strict Transport Security(HSTS)ヘッダーです。WebサイトがHSTSヘッダーを送信すると、ブラウザーは次回以降のアクセスでHTTPSプロトコルを使用します。

しかし、利便性が高く、実装も簡単で、ほぼすべてのブラウザーがサポートしているにもかかわらず、HSTSヘッダーを含むHTTPレスポンスはモバイルで約25%、デスクトップで28%にすぎません。

HSTSヘッダーについて詳しく解説し、Webセキュリティに与える影響とNode.jsで使用すべき理由を見ていきましょう。さらに、Node.jsサーバーでHSTSを有効にする方法も紹介します。

HSTSを有効にする理由

HTTPS接続が安全なのは、SSL/TLS証明書を使って送受信されるデータパケットを暗号化・復号するためです。これらの証明書は、信頼できる認証局(CA)が検証できます。そのため、ネットワーク通信を傍受した人物が誰であっても、送信されたデータパケットを読み取れるのはユーザーのコンピューターとサーバーだけです。

一般的なHTTP接続ではデータが暗号化されないため、データにアクセスできる人は誰でも内容を読み取れます。意図しない受信者には、ユーザーのデータを宛先サーバーへ中継・転送するサーバーや、公衆Wi-Fiルーター上のネットワーク通信にアクセスするハッカーなどが含まれる可能性があります。

Snykも共同執筆したWeb Almanac 2022レポートの調査結果を見てみましょう。

Web Almanac 2022のグラフ。モバイルリクエストの25%にHSTSヘッダーが含まれていることと、デスクトップおよびモバイルにおけるHSTSディレクティブの使用率を示しています。

現在、includeSubdomainディレクティブを含むリクエストへのレスポンスは、モバイルでわずか34%、デスクトップで37%です。

残りの大多数は中間者(MITM)攻撃を受けるリスクがより高く、Node.js WebアプリケーションのWeb認証情報が侵害されるだけでなく、クレジットカード情報やIDの窃取につながる可能性もあります。特に、攻撃者はログイン情報を入力する瞬間にデータを傍受する必要はありません。ブラウザーのCookieにはセッション/アクセストークンが保存・送信されることが多く、攻撃者はセッションハイジャックによってそれらを取得できます。

HSTSの仕組み

HTTPレスポンスヘッダーにHSTSが含まれていると、Webブラウザーは常にサーバーとのHTTPS接続を使用するようになり、最初にHTTPで接続したユーザーも自動的にHTTPSへリダイレクトされます。それ以降、Webアプリケーションやサイトとの接続は暗号化され、安全な状態に保たれます。また、アプリでセキュアCookieを使用できるようになり、MITM攻撃の防止にも役立ちます。

HSTSヘッダーで使用できるパラメーターは3つあります。順に見ていきましょう。

max - age = <expire-time>

この必須パラメーターは、ブラウザーがサイトへのHTTPS接続を記憶する期間を秒単位で指定します。この期間が過ぎると、次回アクセス時にブラウザーは通常どおりサイトを読み込みます。HSTSヘッダーを受信するたびに、ユーザーのブラウザーで有効期限が更新されます。そのため、このパラメーターは無期限に有効な状態を保つことがあります。値を0に設定すれば、HSTSをすぐに無効にすることもできます。

HSTSの設定をテストするときは、300秒(5分)などの短い期間を指定できます。サイトが正しく動作することを確認できたら、31,536,000秒(1年)など、より長い期間に設定しましょう。

includeSubDomains

includeSubDomainsパラメーターは、WebサイトとすべてのサブドメインでHSTSを有効にするようブラウザーに指示するオプションのフラグです。この設定は広範囲に適用されるため、設定する前に、すべてのサブドメインがHTTPSに対応していることを確認してください。

preload

最初の接続では、侵害されたHTTP接続が使用される可能性があります。このリスクに対処するため、Googleは非公式のpreloadパラメーターを運用しています。主要なブラウザーのほとんどがこのセキュリティヘッダーをサポートしており、リスクを大幅に軽減できます。Webサイトでは、HSTSヘッダーにpreloadパラメーターを追加し、ドメインをpreloadサービスに登録できます。ブラウザーはこのリストを参照して、最初の接続でHTTPSを使用できるかどうかを確認します。preloadパラメーターを機能させるには、max-ageを31,536,000秒以上に設定し、includeSubDomainsパラメーターを有効にする必要があります。

preloadサービスにドメインが登録されると、削除リクエストの処理には数か月かかる場合があるので注意してください。ドメインをpreloadに登録する場合は、申請要件と導入に関する推奨事項を確認しましょう。

HSTSでセキュリティを強化する

Node.jsサーバーのコード例を使って、HSTSヘッダーを返す方法を見ていきましょう。この記事で使用するコード例はすべて公開されており、次のGitHubのオープンソースリポジトリで確認できます。

前提条件

このプロジェクトの実行には、以下が必要です。

  • 登録済みのドメイン名

  • Node.jsがインストールされ、ネットワークポート80と433が利用可能なサーバー

  • サーバーを指す、ドメインの有効なDNSレコード

SSL/TLS証明書を取得する

HTTPSを有効にするには、まずドメインの証明書と秘密鍵が必要です。まだ持っていない場合は、Let’s Encryptを利用して、次の手順で取得できます。

まず、手順に従ってcertbotをインストールします。Softwareのドロップダウンからotherを選択し、Systemのドロップダウンからサーバーのオペレーティングシステムを選びます。次に、表示される手順に従ってください。

「その他」とCentOS 8で動作するHTTPウェブサイト向けの選択肢を表示したCertbotの手順ページ。

次に、サーバーでポート80と443が開いていることを確認し、サーバーのドメインを指定して次のコマンドを実行します。

certbot certonly --standalone -d your.domain.url

コマンドの前にsudoを付け、特権ユーザーとして実行する必要がある場合があります。

CertbotがLet’s EncryptのHTTPS証明書を正常に取得し、snykdemo.instafluff.tv用に保存したことを示すターミナル出力。

次に、certbot certificatesを実行し(必要に応じてsudoを付けて)、証明書と秘密鍵のファイルパスを取得します。

snykdemo.instafluff.tv の証明書に関する Certbot の出力を表示するターミナル。証明書の有効期限と、Let's Encrypt の証明書および秘密鍵のパスが表示されています。

サーバーコードをセットアップする

まず、次のコマンドを実行してサーバー上にプロジェクトフォルダーを作成します。mkdir snyk-demo。

次に、cd snyk-demoを実行してフォルダーに移動します。

続いて、npm init -yを実行してフォルダーをNode.jsプロジェクトとして初期化します。

snyk-demoプロジェクトのNode.js package.jsonファイルをnpm initで作成しているターミナル。

次に、サーバーのプロジェクト依存関係をインストールします。ExpressまたはFastifyの該当する手順に従ってください。

Express:

npm install express helmet

Fastify:

npm install fastify @fastify/helmet

ここでは、HTTPとHTTPSを実行する基本的なExpress Node.jsサーバーを作成します。

このフォルダーにindex.jsファイルを作成し、次のNodeモジュールをコードの先頭に追加します。

Express:

const fs = require( "fs" );
const http = require( "http" );
const https = require( "https" );
const express = require( "express" );
const helmet = require( "helmet" );

Fastify:

const fs = require( "fs" );
const helmet = require( "@fastify/helmet" );

次のコードを追加して証明書ファイルを読み込み、certificatePathの値をドメイン証明書のパスに置き換えます。Let’s Encryptのcertbotを使用した場合は、certbot certificatesを実行してファイルパスを取得できます。

Express:

// Replace the following with your domain certificate path
const certificatePath = `/etc/letsencrypt/live/snykdemo.instafluff.tv/;
const privateKey = fs.readFileSync( `${certificatePath}/privkey.pem, "utf8" );
const certificate = fs.readFileSync( `${certificatePath}/fullchain.pem, "utf8" );
// Add the following if your certificate includes a certificate chain
// const ca = fs.readFileSync( `${certificatePath}/chain.pem, "utf8" );

const credentials = {
    key: privateKey,
    cert: certificate,
    // ca: ca, // Add the ca if there is a certificate chain
};

Fastify:

// Replace the following with your own domain certificate path
const certificatePath = `/etc/letsencrypt/live/snykdemo.instafluff.tv/`;
const fastify = require( "fastify" )({
    https: {
        key: fs.readFileSync( `${certificatePath}/privkey.pem, "utf8" ),
        cert: fs.readFileSync( `${certificatePath}/fullchain.pem, "utf8" ),
        // // Add the following if your certificate includes a certificate chain
        // ca: fs.readFileSync( `${certificatePath}/chain.pem, "utf8" ),
    },
});
const fastifyHttp = require( "fastify" )();

次に、Express/Fastifyアプリを初期化し、基本的なルートを作成します。

次のコードでは3つすべてのHSTSパラメーターを使用していますが、最初はmax-ageだけを使ってもよいでしょう。

オプション1:Helmetミドルウェアを使用する

HSTSを設定する1つ目の方法は、アプリの初期化時にHelmetミドルウェアを使うことです。

Express:

const app = express();
app.use( helmet() );
app.use( helmet.hsts( { maxAge: 300, includeSubDomains: true, preload: true } ) );

app.use( ( req, res ) => {
res.send( "Hello secure web!" );
});

Fastify:

const hsts = { maxAge: 300, includeSubDomains: true, preload: true };
fastify.register( helmet, { hsts } );
fastifyHttp.register( helmet, { hsts } );

オプション2:ヘッダーを手動で設定する

もう1つの方法は、すべてのレスポンスに適用されるミドルウェアを使って、HSTSヘッダーを手動で設定することです。

Express:

const app = express();

app.use( ( req, res ) => {
    if( req.secure ) {
         res.setHeader( "Strict-Transport-Security", "max-age=300; includeSubDomains; preload" );
    }
    res.send( "Hello secure web!" );
});

Fastify:

fastify.get( "/", ( req, res ) => {
    res.header( "Strict-Transport-Security", "max-age=300; includeSubDomains; preload" );
    res.send( "Hello secure web!" );
});
fastifyHttp.get( "/", ( req, res ) => {
    res.header( "Strict-Transport-Security", "max-age=300; includeSubDomains; preload" );
    res.send( "Redirect to HTTPS" );
});

最後に、次のコードでHTTPサーバーとHTTPSサーバーを作成します。

Express:

const httpServer = http.createServer( app );
httpServer.listen( 80, () => {
console.log( "HTTP server started." );
});

const httpsServer = https.createServer( credentials, app );
httpsServer.listen( 443, () => {
console.log( "HTTPS server started" );
});

Fastify:

fastifyHttp.listen( { port: 80, host: "0.0.0.0" }, ( err, address ) => {
    console.log( "HTTP server started" );
});

fastify.listen( { port: 443, host: "0.0.0.0" }, ( err, address ) => {
    console.log( "HTTPS server started" );
});

参考として、ExpressとFastifyのプロジェクトコードリポジトリへのリンクを紹介します。

最後に、node index.jsでサーバーコードを実行し、Webブラウザーでドメインを開きます。

bitnami@ip-172-26-14-29:~/snyk-demo$ sudo node index.js
HTTP server started.
HTTPS server started

ブラウザーでDevToolsを開き、Networkタブでレスポンスヘッダーを確認します。HSTSヘッダーが指定されていることを確認できます。

max-age=300、includeSubDomains、preloadを含むStrict-Transport-Securityレスポンスヘッダーを表示したChrome DevToolsのNetworkヘッダー

次のステップ

HTTP Strict Transport SecurityヘッダーがNode.jsアプリケーションのセキュリティにとって重要な理由や、MITM攻撃の防止にどう役立つか、Node.jsコードでの実装方法を理解したことで、ユーザーのWebアプリケーションを保護するための知識が身につきました。

最後に、もう1つ重要な点があります。本番環境では、HSTSの設定やSSLの提供をNode.jsアプリで行わないでください。代わりに、これらの処理はNGINXやApacheなどの静的ファイルHTTPサーバーに任せましょう。

続きを読む

blog feature snyk container custom base recommendations
Blog

Evo Agentic AppSecのご紹介:AI開発のスピードに対応するセキュリティ

AI駆動型開発のスピードに、従来のセキュリティワークフローは追いついていません。SnykのAgentic AppSecは、セキュリティエージェントが脆弱性を継続的に検出、修正、検証し、人が基準を定められるようにします。

blog feature ai violet
Blog

VulnBump:AIエージェントはビルドを壊さずに脆弱な依存関係を修正できるか?

VulnBumpは、実際の依存関係アップグレード307件を対象にAIエージェントをテストします。脆弱性が解消され、ビルドとテストが成功した場合にのみ、修正としてカウントされます。

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。