💡

expressアプリケーションでhelmetを使用した場合と使用しない場合のレスポンスヘッダーの比較

2024/10/30に公開

Helmetとは

使用目的

  • HTTPセキュリティヘッダーの自動設定
  • 一般的なウェブ脆弱性からの保護
  • セキュリティベストプラクティスの簡単な実装

対応する主なセキュリティ課題

  • クロスサイトスクリプティング(XSS)
  • クリックジャッキング
  • MIMEタイプスニッフィング
  • HTTPSの強制(HSTS)
  • その他の一般的なウェブセキュリティ問題

サンプル作って動かしてみる

app.ts

import express from "express";
import helmet from "helmet";

const app = express();
const port = 3000;

// Helmetを使用するルート
app.get("/with-helmet", helmet(), (req, res) => {
  res.send("ヘルメット被っています!");
});

// Helmetを使用しないルート
app.get("/without-helmet", (req, res) => {
  res.send("ヘルメット被っていない!");
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});
  • 通常はミドルウェアに定義して全てのrouteで適用させるのだが、今回は違いを見るために、個別にhelmetを使用しています!

helmet使っていないレスポンスヘッダー

connection: keep-alive
date: Sun, 20 Oct 2024 07:54:44 GMT
etag: W/"24-Q9xWLX9VPYRhI5AHyGKlFzG1wUQ"
keep-alive: timeout=5
x-powered-by: Express

helmet使っているレスポンスヘッダー

connection: keep-alive
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-opener-policy: same-origin
cross-origin-resource-policy: same-origin
date: Sun, 20 Oct 2024 08:03:26 GMT
etag: W/"24-mYQ3A4bD/Lt1rSmVacZJs5ikdJA"
keep-alive: timeout=5
origin-agent-cluster: ?1
referrer-policy: no-referrer
strict-transport-security: max-age=31536000; includeSubDomains
x-content-type-options: nosniff
x-dns-prefetch-control: off
x-download-options: noopen
x-frame-options: SAMEORIGIN
x-permitted-cross-domain-policies: none
x-xss-protection: 0
  • helmetありだと明らかに多くなっていますね
  • x-powered-by は非表示になっています

ぱっと見で違いが分かりにくいので、比較表作りました!

Helmetの使用有無によるヘッダーの比較表

レスポンスヘッダー Helmet未使用 Helmet使用
connection keep-alive keep-alive
date Sun, 20 Oct 2024 07:54:44 GMT Sun, 20 Oct 2024 08:03:26 GMT
etag W/"24-Q9xWLX9VPYRhI5AHyGKlFzG1wUQ" W/"24-mYQ3A4bD/Lt1rSmVacZJs5ikdJA"
keep-alive timeout=5 timeout=5
x-powered-by Express -
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-opener-policy - same-origin
cross-origin-resource-policy - same-origin
origin-agent-cluster - ?1
referrer-policy - no-referrer
strict-transport-security - max-age=31536000; includeSubDomains
x-content-type-options - nosniff
x-dns-prefetch-control - off
x-download-options - noopen
x-frame-options - SAMEORIGIN
x-permitted-cross-domain-policies - none
x-xss-protection - 0

設定の詳細

  • Helmet使用時は x-powered-by ヘッダーが削除され、サーバー情報の露出を防いでいる
    • ドキュメントにも、もしhelmetを使用しないなら、app.disable('x-powered-by') を使用して非表示にするのがベストプラクティスと書かれていましたね!
  • Content-Security-Policy (CSP)
    • ヘッダー: 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
    • 目的: XSSやデータインジェクション攻撃などを防ぐため、ウェブページが読み込めるリソースを制限する
    • 効果:
      • スクリプトは同一オリジンからのみ許可
      • スタイルは同一オリジンとHTTPS、およびインラインスタイルを許可
      • 画像は同一オリジンとdata URIのみ許可
      • フォントは同一オリジン、HTTPS、およびdata URIから許可
      • オブジェクトの埋め込みを禁止
      • フォーム送信は同一オリジンのみ許可
      • フレームの埋め込みは同一オリジンのみ許可
  • Cross-Origin-Opener-Policy
    • ヘッダー: cross-origin-opener-policy
    • 値: same-origin
    • 目的: 異なるオリジンのページから開かれた場合に、ページ間の直接の参照を防ぐ
    • 効果: クロスオリジン攻撃からの保護を強化し、サイト分離を促進する
  • Cross-Origin-Resource-Policy
    • ヘッダー: cross-origin-resource-policy
    • 値: same-origin
    • 目的: リソースが同一オリジンからのみ読み込まれることを保証する
    • 効果: クロスオリジンのリソース読み込みを制限し、情報漏洩を防ぐ
  • Referrer-Policy
    • ヘッダー: referrer-policy
    • 値: no-referrer
    • 目的: ブラウザがリファラ情報をどのように送信するかを制御する
    • 効果: リファラ情報を完全に送信しないようにし、プライバシーを保護する
    • MDN Referrer-Policy読むと詳しくわかる
  • HTTP-Strict-Transport-Security (HSTS)
    • ヘッダー: strict-transport-security
    • 値: max-age=31536000; includeSubDomains
    • 目的: HTTPS接続を強制する
    • 効果:
      • ブラウザにHTTPS接続のみを使用するよう指示
      • 1年間(31536000秒)有効
        • こちらの有効期限が切れてHTTPSを強制されない状態というのはまず起こり得ない
          • 有効期限が切れてもすぐには HTTPS 強制を解除しない
          • 継続的に更新される
      • サブドメインにも適用
  • X-Content-Type-Options
    • ヘッダー: x-content-type-options
    • 値: nosniff
    • 目的: ブラウザによるMIMEタイプのスニッフィングを防止する
      • MIMEタイプスニッフィングとは、ブラウザがサーバーから送られてきたコンテンツのMIMEタイプを無視して、独自に内容を解析し、タイプを推測する行為
        • MIMEタイプとは、文書、ファイル、またはバイト列の性質や形式を示す標準で、 ブラウザにファイルの中身を正しく解釈させる為には、ファイル拡張子ではなく、このMIMEタイプを使用する
    • 効果: コンテンツタイプの誤認識によるセキュリティリスクを軽減する
  • X-DNS-Prefetch-Control
    • ヘッダー: x-dns-prefetch-control
    • 値: off
    • 目的: ブラウザのDNSプリフェッチを制御する
      • DNSプリフェッチとは、ブラウザがページ内のリンクのドメイン名を事前に解決しておく機能
        • onの時: ユーザーがリンクをクリックする前に、そのドメインのDNS解決が行われる
        • offの時: ユーザーがリンクをクリックするまで、DNSの解決は行われない
    • 効果: ユーザーのプライバシーを保護し、不要なDNSルックアップを防ぐ
      • DNSルックアップとは、DNSを使用して必要な情報を検索すること
  • X-Download-Options
    • ヘッダー: x-download-options
    • 値: noopen
    • 目的: Internet Explorer 8 以降でダウンロードしたファイルの自動実行を防ぐ
      • Chrome、Safari、Firefoxなどのモダンブラウザでは、このヘッダーを無視する
    • 効果: ダウンロードしたファイルによる潜在的な攻撃を軽減する
      • ダウンロードしたファイルが自動的に「開く」オプションを提示するのを防ぐ
        • これにより、ユーザーが意図せずにダウンロードしたファイルを実行してしまうリスクを軽減する
  • X-Frame-Options
    • ヘッダー: x-frame-options
    • 値: SAMEORIGIN
    • 目的: クリックジャッキング攻撃を防止する
      • クリックジャッキングとは、Webサイト上に隠蔽・偽装したリンクやボタンを設置し、サイト訪問者を視覚的に騙してクリックさせるなど意図しない操作をするよう誘導させる手法
    • 効果: ページを同一オリジンのフレーム内でのみ表示を許可する
  • X-Permitted-Cross-Domain-Policies
    • ヘッダー: x-permitted-cross-domain-policies
    • 値: none
    • 目的: Adobe製品(主にFlash)のクロスドメインポリシーを制御する
    • 効果: クロスドメインデータ読み込みを完全に禁止する
  • X-XSS-Protection
    • ヘッダー: x-xss-protection
    • 値: 0
    • 目的: ブラウザ組み込みのXSS保護機能を制御する
    • 効果: 最新のブラウザでは非推奨となったため、この機能を無効にするため、0を指定している。代わりにCSPを使用してXSS攻撃から保護する

カスタム方法

app.use(helmet({
  // Content Security Policy (CSP) の設定
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"], // デフォルトでは同一オリジンのみ許可
      scriptSrc: ["'self'", "'unsafe-inline'", 'example.com'], // スクリプトソースの指定
      styleSrc: ["'self'", 'styles.example.com'], // スタイルソースの指定
      imgSrc: ["'self'", 'images.example.com'], // 画像ソースの指定
    },
  },

  // Referrer Policy の設定
  referrerPolicy: {
    policy: 'strict-origin-when-cross-origin', // クロスオリジンの場合はオリジンのみを送信
  },

  // HTTP Strict Transport Security (HSTS) の設定
  hsts: {
    maxAge: 31536000, // HSTS ヘッダーの有効期間(秒)
    includeSubDomains: true, // サブドメインにも適用
    preload: true, // ブラウザの HSTS プリロードリストに登録
  },

  // X-Frame-Options の設定
  frameguard: {
    action: 'sameorigin', // 同一オリジンからのフレーム埋め込みのみ許可
  },

  // X-XSS-Protection の無効化(CSPで代替)
  xssFilter: false,

  // X-Content-Type-Options の設定
  noSniff: true, // MIMEタイプスニッフィングの防止

  // DNSプリフェッチの制御
  dnsPrefetchControl: {
    allow: false, // DNSプリフェッチを無効化
  },

  // クリックジャッキング保護の設定
  frameguard: {
    action: 'deny', // フレームでの表示を完全に禁止
  },

  // 特定のヘッダーを完全に無効化する例
  // contentSecurityPolicy: false, // CSPを完全に無効化(非推奨)
}));
  • この辺り、実際のアプリケーションに合わせて設定する必要があります

最後に

今回の記事を書くに当たってまずやったのは、XSSの復習からでした!
と言っても、まだ深い理解に到達できていない気がして、ここも深掘りしてアウトプットしていこうと思っています!
私は過去に、調理師をしていたことがあるのですが、作ったものを安全に楽しく食事をしてもらうためには最低限の食材に対する知識(賞味期限や保存方法など)・衛生管理の知識が必要でした!

それがエンジニアでいうところのセキュリティの知識なのかと感じてます。安全に楽しくサービスを使ってもらうための知識なんだと。。

現状は、まだ表面をなぞっただけの知識なので、ユーザの方に安全に楽しく触ってもらえるサービスを作るために、セキュリティの学習を継続して深掘りしていきます!

参考

バックテック【ヘルステック系スタートアップの試行錯誤】

Discussion