X-Content-Type-Options: nosniff 解説|MIMEスニッフィング対策

(更新: 2026年6月21日 ) セキュリティ HTTPヘッダー MIME Web XSS
結論
  • X-Content-Type-Options: nosniff は、ブラウザに「Content-Type を推測するな」と命じる一行の防御だ。
  • Internet Explorer 系で問題化した MIME スニッフィングは、モダンブラウザでも完全には消えていない。
  • 正しい Content-Type を返すことが前提のうえで、HTML・JSON・静的アセットの全レスポンスに nosniff を付ける。
  • CSP や HSTS と並べて セキュリティヘッダー一覧 の定番セットに入れるのが最短ルートだ。

X-Content-Type-Options: nosniff とは

HTTP レスポンスには Content-Type ヘッダーがあり、ブラウザに「このバイト列は HTML か、JavaScript か、画像か」を伝える。X-Content-Type-Options は、その宣言を推測で上書きさせないためのレスポンスヘッダーだ。

現在定義されている値は nosniff のみ。設定例は次のとおり。

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-Content-Type-Options: nosniff

仕様上は非標準ヘッダー(X- プレフィックス)だが、Chrome・Firefox・Safari・Edge のすべてが実装しており、Mozilla Observatory や OWASP のチェックリストでも必須項目として扱われる。CDN・リバースプロキシ・アプリのどの層で付けても効果は同じだが、オリジンから一貫して返すのが監査上わかりやすい。

nosniff は「魔法の XSS 対策」ではない。サーバーが間違った Content-Type を返している限り、nosniff はその間違いを正してくれない。あくまで「宣言どおりに扱え、script や style として実行するな」という最後の安全弁として位置づける。

仕様と歴史

X-Content-Type-Options は Microsoft が Internet Explorer 8 向けに導入したヘッダーで、後に Fetch Living Standard に取り込まれた。値は nosniff 以外は定義されておらず、未知の値は無視される。

項目内容
ヘッダー名X-Content-Type-Options
有効な値nosniff のみ
適用対象HTTP レスポンス(リクエストには効かない)
主な効果MIME スニッフィングの抑制、script/style の厳格な MIME チェック
標準化WHATWG Fetch Standard に記載(非 IETF RFC)

X- プレフィックスは非推奨」という一般的な HTTP ヘッダーの命名規則にもかかわらず、実装・ツール・監査のすべてがこの名前を前提にしている。改名の議論はあるが、実務では X-Content-Type-Options をそのまま使うのが正解だ。

MIME スニッフィング(コンテンツタイプスニッフィング)の仕組み

MIME スニッフィングとは、ブラウザが Content-Type ヘッダーとファイル実体が食い違うとき、先頭バイト列や拡張子から「本当の種類」を推測する挙動のことだ。正式には content type sniffing と呼ばれ、Fetch 仕様の「MIME type sniffing」アルゴリズムに整理されている。

ブラウザが推測に使う手がかり

推測の典型パターンは次のとおり。

  • マジックバイト(ファイルシグネチャ) — PNG は 89 50 4E 47、PDF は %PDF、ZIP は PK など、先頭数バイトで種類を判定
  • HTML トークン<html<script<!DOCTYPE<svg などが見えれば HTML または XML 扱い
  • 拡張子ヒント — URL パスや Content-Disposition の filename から .html を拾う
  • polyglot ファイル — JPEG としても HTML としても解釈できるバイト列を意図的に作る
  • BOM(Byte Order Mark) — UTF-8 BOM EF BB BF の有無でテキスト判定に影響

モダンブラウザは Fetch 仕様に沿ってスニッフィングを大幅に抑制している。とはいえ 「ヘッダーと実体の不一致」 は、セキュリティ監査・ペネトレーション・バグ報奨プログラムで今も指摘される。nosniff は HTTP レイヤーで「推測禁止」を明示的に宣言できる rare な手段だ。

スニッフィングが許可される条件(概要)

Fetch 仕様では、おおよそ次のような場合にスニッフィングが行われる(nosniff がない場合)。

条件スニッフィングの可能性
Content-Type が欠落高い — default_type や拡張子から推測
Content-Type: application/octet-stream高い — 汎用バイナリは推測対象
Content-Type: text/plain中〜高 — HTML トークン検出で HTML 化
Content-Type: unknown/xxx高い — 未知の MIME は推測
Content-Type: image/jpeg だが中身が HTML低〜中 — ブラウザ・文脈依存
X-Content-Type-Options: nosniff あり原則禁止 — 宣言 MIME を尊重
script と style は特に厳格

Chrome 等では nosniff 付きレスポンスにおいて、JavaScript として実行可能な MIME タイプ以外text/plainapplication/octet-stream など)の script はブロックされる。CSS も同様に text/css 以外は stylesheet として適用されない。Console には Refused to execute script ... because its MIME type ('text/plain') is not executable のようなメッセージが出る。

なぜブラウザはスニッフィングするのか

歴史的には、Web サーバーが MIME を正しく返さないことが日常茶飯事だった。.jstext/plain で返る、.cssapplication/octet-stream になる——こうした「壊れたサイト」を動かすため、ブラウザは寛容な推測ロジックを持っていた。

セキュリティと互換性のトレードオフは今も続いている。nosniff を全サイトに強制すると、MIME 未設定のレガシー資産が一斉に壊れる。だからこそサイト運営者側が正しい Content-Type を返し、nosniff で宣言を守らせるという分担が現代のベストプラクティスになっている。

MIME スニッフィング攻撃の全体像

MIME スニッフィング攻撃とは、サーバーが返した Content-Type と異なる種類としてブラウザに解釈させ、意図しないコード実行やデータ窃取を起こす攻撃クラスの総称だ。XSS の一種として分類されることも多いが、注入ポイントは「HTML テンプレート」ではなく「ファイルの配信 MIME」にある点が特徴的だ。

攻撃が成立する前提条件

次の条件が揃うと、MIME スニッフィング経由の XSS が成立しやすい。

  1. 攻撃者がファイル内容をコントロールできる(アップロード、プロフィール欄、コメント添付など)
  2. サーバーが誤った Content-Type で再配信する(拡張子だけ見る、マジックバイト未検証)
  3. 同一オリジン上でその URL を読み込める<script src><iframe><object>、直リンク)
  4. nosniff がない、またはブラウザがスニッフィングする(古い IE、設定ミス)

Cookie の SameSite や CSP があっても、同一オリジン内の script 実行は制限が緩い。MIME スニッフィングで「同一オリジン上の text/plain ファイルを script として実行」できれば、document.cookiefetch('/api/me') が通る。

攻撃フロー(抽象)

[攻撃者] 悪意あるコンテンツをアップロード or 保存

[サーバー] 拡張子・推測のみで Content-Type を決定(例: image/jpeg)

[被害者] 同一オリジン URL をブラウザが読み込む

[ブラウザ] スニッフィングで HTML/JavaScript として解釈

[結果] セッション窃取・DOM 操作・フィッシング UI 注入

Internet Explorer 時代の攻撃シナリオ(レガシー)

MIME スニッフィングが「教科書的な脆弱性」として語られる背景には、Internet Explorer 6〜11 の aggressive なスニッフィングがある。IE は次のような条件で、宣言された Content-Type を無視して HTML として解釈した。

  1. レスポンスが Content-Type: text/plain または Content-Type: application/octet-stream
  2. ファイル先頭付近に HTML らしいマーカー(<script><html など)が存在
  3. 同一オリジン上で <iframe src="/uploads/evil.txt"><object data="..."> 経由で読み込まれる

典型攻撃:アップロード + iframe 埋め込み

2000年代の掲示板・メール添付・プロフィール画像アップロードで、次のチェーンが何度も再現された。

[攻撃者] evil.jpg として HTML+script をアップロード

[サーバー] Content-Type: image/jpeg で保存・配信(拡張子だけ見ている)

[IE] 先頭に HTML トークンを検出 → HTML として解釈

[被害者] 同一オリジン iframe 内で script 実行 → Cookie 窃取

攻撃者が仕込むファイルは polyglot であることが多い。JPEG のマジックバイト FF D8 FF で始まりつつ、コメント領域や末尾に <script>...</script> を埋め込む。サーバー側の MIME 判定は「JPEG っぽい」と image/jpeg を返すが、IE は HTML として実行する。

polyglot ファイルの作り方(防御者向け理解)

攻撃者視点の知識だが、防御設計には必須だ。

手法概要サーバー側の見え方
JPEG + HTMLFF D8 FF 後に JPEG コメントセクションへ HTML を埋め込むimage/jpeg と判定されやすい
GIF + HTMLGIF89a ヘッダー + 最小フレーム + 末尾に scriptimage/gif
PDF + JavaScriptPDF 内の JS オブジェクト(別系統の脅威)application/pdf
ZIP 偽装先頭を PK で始め拡張子を .zip にアーカイブ扱い

マジックバイトと拡張子の一致だけでは不十分だ。ファイル全体をスキャンし、HTML トークンや script パターンを含むアップロードは拒否するのが安全な設計だ。

IE 固有の「ダウンロード後実行」問題

IE には 「ダウンロードしたファイルをブラウザ内で開く」 挙動も絡んだ。Content-Disposition: attachment がなく、text/plain で HTML 断片を返すと、ユーザーが「テキストファイル」だと思って開いた瞬間に script が走る事例も報告されている。nosniff はこの経路でも「plain として扱え、HTML として解釈するな」と指示する。

IE 互換は不要でも nosniff は有効

Internet Explorer のサポート終了後も、nosniff はモダンブラウザの script/style ブロック、JSON スニッフィング抑制、セキュリティスキャナーの要件として価値を持つ。レガシー対策のつもりで入れておくと、結果的に今の Chrome でも効く典型例だ。

現代でも起きうる攻撃シナリオ

IE を引きずらなくても、ファイルアップロード + 再配信の設計ミスは今も XSS の温床になる。バグバウンティでは「Uploaded HTML served as text/plain → XSS」系の報告が定期的に受理される。

シナリオ A:text/plain 配信のユーザー生成コンテンツ

社内 Wiki やサポートチケットで、ユーザーが .txt をアップロードできるとする。サーバーは /files/note.txtContent-Type: text/plain で返す。攻撃者は中身を次のようにする。

<script>
fetch('/api/me', { credentials: 'include' })
  .then(r => r.json())
  .then(data => { /* セッション情報を外部へ */ });
</script>

nosniff がなく、かつブラウザや埋め込み WebView の実装次第では HTML として解釈されうる。同一オリジンなので Cookie 付き fetch が通り、セッション窃取に直結する。

nosniff ありでは、<script src="/files/note.txt"> は MIME エラーでブロックされる。ただし HTML として直接ナビゲート(アドレスバーで開く)や <iframe src> の挙動は文脈依存なので、危険なコンテンツはそもそも保存しないことが根本対策だ。

シナリオ B:CDN の default_type 任せ

nginx や S3 互換ストレージで MIME マップが未設定だと、.jsapplication/octet-stream で返ることがある。CSP や ES module の import は厳密な MIME を要求するため、機能障害とセキュリティの両方で問題になる。nosniff 付きでは octet-stream の script は実行されない——正しい MIME を直す必要がある

# 確認例
curl -sI https://example.com/assets/app.mjs | grep -i content-type
# Content-Type: application/octet-stream  ← 要修正

シナリオ C:JSONP 時代の残骸

application/javascripttext/javascript で JSON 風データを返し、<script src="/api/data"> で読み込むパターン(JSONP)は、XSS フィルタを迂回する手段として悪用された歴史がある。nosniff と CSP の script-src 制限は、このクラスの「MIME すり替え」を封じる。

シナリオ D:S3 / オブジェクトストレージの Content-Type 未設定

AWS S3 にアップロードした際、Content-Type メタデータを付け忘れると application/octet-stream になる。CloudFront 経由で配信すると、ブラウザがスニッフィングしたり、module スクリプトが拒否されたりする。アップロード時に MIME を明示し、配信時に X-Content-Type-Options: nosniff を CDN で足すのが定石だ。

シナリオ E:開発者ツール・ステージングの設定漏れ

本番 nginx には nosniff があるが、ステージングやプレビュー環境の vite previewpython -m http.server には付いていない——というパターンもある。攻撃者がステージング URL を標的にするケースは少ないが、全環境で一貫したヘッダーが監査の要件になることは多い。

nosniff が変えるブラウザ挙動

X-Content-Type-Options: nosniff を付与すると、主要ブラウザはおおよそ次のように振る舞う。

リソース種別nosniff なし(概要)nosniff あり
<script src>非 script MIME でも実行される場合があるscript として許可された MIME 以外はブロック
<link rel="stylesheet">CSS 以外が stylesheet として効く場合があるtext/css 以外は拒否
<img>image/* 以外から HTML スニッフィングの余地image/* を HTML として解釈しない
fetch/XHR の JSONtext/plain 等へのスニッフィングapplication/json 等の宣言を尊重
Worker / Service Worker厳格な MIME 要求(もともと厳しい)変更なし(元から厳格)
ES Module (type="module")MIME 誤りで実行失敗しやすい誤 MIME は確実にブロック

JavaScript として許可される MIME(Chrome 系)

Fetch 仕様および Chromium の実装では、classic script として実行可能な MIME はホワイトリスト方式だ。代表例:

  • text/javascript
  • application/javascript
  • application/x-javascript
  • text/ecmascript
  • application/ecmascript

text/plainapplication/json実行不可。ES module はさらに text/javascript 系に限定される実装が一般的だ。

重要:nosniff は「配信レスポンス」に付ける。リクエストの Content-Type(POST ボディ)には効かない。ファイルアップロード POST より、GET で再配信するときのヘッダーが焦点になる。

正しい Content-Type 指定一覧

nosniff の前提は、種類ごとに正確な MIME タイプと charset を返すことだ。よく使う値を表にまとめる。

Web ページ・アプリケーション資産

種類推奨 Content-Typecharset補足
HTML 文書text/htmlcharset=utf-8 必須推奨charset 省略は文字化け・XSS の温床になりうる
XHTMLapplication/xhtml+xmlcharset=utf-8text/html より厳格な XML パーサ
CSStext/csscharset=utf-8 推奨@import や module 連携で MIME 厳格化
JavaScript(classic)text/javascriptcharset=utf-8 推奨歴史的に application/javascript も可
JavaScript(ES module)text/javascript任意module スクリプトは MIME チェックが厳しい
JSON APIapplication/jsoncharset=utf-8 推奨text/json は非推奨
JSON-LDapplication/ld+jsoncharset=utf-8SEO 構造化データ
XMLapplication/xmlcharset=utf-8XXE 対策は別途必要
WebAssemblyapplication/wasmなしoctet-stream だとインスタンス化失敗
Source Mapapplication/json.map ファイル
Markdown(配信時)text/plain または text/markdownutf-8HTML 変換するなら変換後に text/html

画像・メディア

種類推奨 Content-Type補足
PNG 画像image/pngマジックバイト 89 50 4E 47 と整合
JPEG 画像image/jpegEXIF 付きでも MIME は jpeg
GIF 画像image/gifアニメ GIF も同じ MIME
WebPimage/webp古い nginx では types 追加が必要
AVIFimage/avif比較的新しい。MIME マップ要確認
SVGimage/svg+xmlXSS 载体になりうる——サニタイズか配信禁止を検討
ICOimage/x-icon または image/vnd.microsoft.iconファビコン
MP4 動画video/mp4Range リクエストと整合
WebM 動画video/webm
MP3 音声audio/mpeg
Oggaudio/ogg または video/ogg中身に応じて使い分け

ドキュメント・ダウンロード

種類推奨 Content-Type補足
PDFapplication/pdfインライン表示時は CSP も確認
ZIP アーカイブapplication/zip実行可能内容の再配信は別問題
汎用ダウンロードapplication/octet-streamContent-Disposition: attachment を併用
CSVtext/csvcharset=utf-8 推奨
プレーンテキストtext/plainユーザー生成なら HTML トークン検査

フォント

種類推奨 Content-Type補足
WOFFfont/woffCORS ヘッダーとセット
WOFF2font/woff2現行の主流
TTF / OTFfont/ttf / font/otf
EOT(レガシー)application/vnd.ms-fontobjectIE 専用。新規では不要

拡張子からの推測だけに頼ると、.wasm が octet-stream になる、.mjs が text/plain になる、といった事故が起きる。ビルド成果物ごとに MIME を明示し、デプロイ後に DevTools の Network で spot check する。

# nginx: 不足している MIME の例
types {
    application/wasm              wasm;
    text/javascript               js mjs;
    image/webp                    webp;
    image/avif                    avif;
    font/woff2                    woff2;
}
application/octet-stream の誤用

「わからないファイルは全部 octet-stream」は、ダウンロード専用なら正しいが、ブラウザ内で解釈されるパス/static//uploads/)では危険だ。octet-stream + nosniff でも、古いクライアントや <embed> 経由で想定外の挙動が残る可能性がある。ユーザー生成ファイルには attachment を強制する。

multipart/form-data と boundary の落とし穴

ファイルアップロードは Content-Type: multipart/form-data; boundary=----WebKitFormBoundary... 形式で送られる。boundary はパート(フィールドやファイル)を区切る文字列だ。nosniff とは別レイヤーだが、multipart の設計ミスは「種類の誤判定」やパーサ混乱につながり、結果としてセキュリティ事故の入口になる。

boundary 不一致・欠落

問題症状セキュリティ上のリスク
boundary= パラメータ欠落サーバーが 400 を返すか、ボディ全体を1フィールド扱い想定外のパースで検証ロジックを bypass
宣言 boundary とボディ不一致パート境界がずれ、ファイル内容が別フィールドに混入ファイル名偽装・メタデータ改ざん
boundary に予約文字" や改行を含む boundary でパーサが混乱古いフレームワークでクラッシュや DoS
クライアント偽装 Content-Typemultipart 宣言だが実体は JSONミドルウェアの二重パースで意図しない処理
巨大パート・パート数過多メモリ枯渇DoS

サーバー側の実装原則:

  • boundary はサーバーが生成する必要はない(クライアントが送る)が、パースライブラリに任せ、自前で split しない
  • 各パートの Content-Type参考情報——filename="evil.php.jpg"Content-Type: image/jpeg は信頼しない
  • マジックバイト + 許可リストで実体を判定し、保存時に拡張子を正規化する
  • 再配信時は検証済み MIME のみ返し、HTML 系(text/html、image/svg+xml、application/xhtml+xml)は拒否する
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----Boundary123

------Boundary123
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: image/jpeg

<バイナリ本体>
------Boundary123--

再配信 GET レスポンス側では、ここで決めた MIME に加え X-Content-Type-Options: nosniff を必ず付ける。アップロード POST レスポンス(302 リダイレクト等)にも付けておくと、スキャナーの指摘を一括で消せる。

SVG と HTML はアップロード段階で拒否

image/svg+xml は XML ベースで script を含められる。nosniff があっても、許可リストに SVG を入れないのが安全。どうしても必要ならサニタイズパイプラインを別途構築する。

nginx での設定例

nosniff は全レスポンスに付けるのが定石。エラーページ(404/500)やリダイレクト応答にも届くよう、always 相当の設定にする。

基本設定(server ブロック全体)

server {
    listen 443 ssl http2;
    server_name example.com;

    # 全レスポンスに nosniff(4xx/5xx 含む)
    add_header X-Content-Type-Options "nosniff" always;

    include       mime.types;
    default_type  application/octet-stream;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

always を付けないと、nginx が生成する 4xx/5xx レスポンスにはヘッダーが載らない。セキュリティスキャンはエラーページも見るため、always は必須に近い。

location 継承の落とし穴

nginx ではlocationadd_header を書くと、親の add_header がすべて消える(同一レベルで上書きされるわけではなく、子で宣言したヘッダーだけが残る)。

server {
    add_header X-Content-Type-Options "nosniff" always;

    location /api/ {
        proxy_pass http://backend;
        # ここで add_header を書くと、親の nosniff が消える!
        add_header X-Content-Type-Options "nosniff" always;  # 子でも必ず再宣言
        add_header Cache-Control "no-store" always;
    }
}

運用ルール: nosniff を付ける location ブロックすべてで always 付きで再宣言するか、more_set_headers(headers-more モジュール)で一元管理する。

静的ファイルと MIME マップ

http {
    include       mime.types;
    default_type  application/octet-stream;

    types {
        text/javascript    js mjs;
        application/wasm   wasm;
        image/webp         webp;
        image/avif         avif;
        font/woff2         woff2;
    }

    server {
        add_header X-Content-Type-Options "nosniff" always;

        location /static/ {
            alias /var/www/static/;
            expires 30d;
            # 親の add_header が効く(子で add_header しない限り)
        }

        location ~* \.(jpg|jpeg|png|gif|webp|avif)$ {
            expires 7d;
            add_header X-Content-Type-Options "nosniff" always;
            add_header Cache-Control "public, immutable";
        }
    }
}

リバースプロキシ(API バックエンド)

location /api/ {
    proxy_pass         http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header   Host $host;
    proxy_set_header   X-Real-IP $remote_addr;

    # バックエンドがヘッダーを返さない場合の保険
    add_header X-Content-Type-Options "nosniff" always;
}

バックエンドと nginx の両方で nosniff を付けると重複するが、同じ値なら問題にならない。監査上は「どちらか一方で一元管理」とするチームも多い。

ユーザーアップロードの配信

location /uploads/ {
    alias /var/www/uploads/;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Content-Disposition "attachment" always;
    # インライン表示を禁止し、ダウンロード専用にする
}

Content-Disposition: attachment は nosniff と別軸の防御だが、ユーザー生成ファイルではセットで検討する価値が高い。

Express(Node.js)での設定例

Express ではミドルウェアで全ルートに付与するのが基本。静的ファイル・API・エラーハンドラすべてをカバーする。

手動ミドルウェア

import express from 'express';

const app = express();

// 最初のミドルウェアとして登録
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  next();
});

app.get('/api/user', (req, res) => {
  res.type('json'); // Content-Type: application/json; charset=utf-8
  res.json({ id: 1, name: 'example' });
});

app.use(express.static('public', {
  setHeaders(res, filePath) {
    res.setHeader('X-Content-Type-Options', 'nosniff');
    // express.static は拡張子から Content-Type を推測する
    // .mjs が text/plain になる場合は mime パッケージで拡張登録
  },
}));

app.listen(3000);

helmet を使う場合(推奨)

import express from 'express';
import helmet from 'helmet';

const app = express();

// nosniff のみ
app.use(helmet.noSniff());

// または他のセキュリティヘッダーと一括
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
    },
  },
  // noSniff はデフォルトで有効
}));

app.listen(3000);

helmet v7 以降は helmet() だけで X-Content-Type-Options: nosniff が有効になる。個別に無効化する場合のみ helmet({ xContentTypeOptions: false }) とする。

ファイル送信と MIME 明示

import path from 'node:path';
import express from 'express';

const app = express();
app.use(helmet());

app.get('/download/:id', async (req, res) => {
  const file = await getVerifiedFile(req.params.id);
  // DB に保存した検証済み MIME を使う(拡張子を信頼しない)
  res.type(file.mimeType);
  res.setHeader('X-Content-Type-Options', 'nosniff');
  if (file.forceDownload) {
    res.setHeader('Content-Disposition', 'attachment; filename="safe-name.bin"');
  }
  res.sendFile(path.resolve(file.storagePath));
});

エラーハンドラでもヘッダーを維持

app.use((err, req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.status(500).type('json').json({ error: 'Internal Server Error' });
});

グローバルミドルウェアを最初に登録していれば、エラーレスポンスにも通常は載る。ただし res.headersSent のタイミングには注意し、helmet を app の先頭に置くのが確実だ。

Fastify との比較(参考)

import Fastify from 'fastify';
import helmet from '@fastify/helmet';

const app = Fastify();
await app.register(helmet, { contentSecurityPolicy: false });
// @fastify/helmet も nosniff を付与

app.get('/api/data', async () => ({ ok: true }));
await app.listen({ port: 3000 });

Express では res.sendFileres.json もミドルウェアの後に実行すればヘッダーが載る。API 専用サーバーでも JSON レスポンスに nosniff を付ける——コストゼロでスキャナースコアが上がる。

Cloudflare など CDN での設定

オリジンを触れない場合や、全レスポンスに統一したい場合は CDN 層で付与する。

Cloudflare Transform Rules(レスポンスヘッダー変換)

ダッシュボード: Rules → Transform Rules → Modify Response Header

  • Operation: Set static
  • Header name: X-Content-Type-Options
  • Value: nosniff

全ホスト名・全パスに適用するルールを1本作れば、オリジンがヘッダーを返さなくてもカバーできる。

Cloudflare Workers(例)

export default {
  async fetch(request, env, ctx) {
    const response = await fetch(request);
    const headers = new Headers(response.headers);
    headers.set('X-Content-Type-Options', 'nosniff');
    return new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers,
    });
  },
};

オリジンと CDN の二重付与は問題にならない(同じ値の重複ヘッダー)。値が矛盾する場合のみブラウザ挙動が不定になるので、値は nosniff で統一する。

他のセキュリティヘッダーとの多層防御

nosniff 単体では反射型 XSS やクリックジャッキングは防げない。役割分担を理解してセットで入れる。

ヘッダー・設定 役割
X-Content-Type-Options: nosniff MIME スニッフィング禁止。宣言外の script/style 実行を抑止
正しい Content-Type nosniff の前提。種類ごとに charset も明示
Content-Security-Policy スクリプト実行元の許可リスト。nosniff と矛盾しない
Content-Disposition: attachment ダウンロード専用ファイルをブラウザ内表示させない
Cross-Origin-Resource-Policy 他オリジンからの読み込み制限(別軸)
Strict-Transport-Security HTTPS 強制。MIME 問題とは独立

CSP は「どこから script を読むか」を制限し、nosniff は「このレスポンス本体を script として実行するな」を MIME レイヤーで縛る。両方設定しても矛盾しない

全体像と nginx・Cloudflare での一括設定は セキュリティヘッダー一覧と設定ガイド を参照。CSP の具体値は CSP 設定ガイド に詳述している。XSS 全般の防御は XSS攻撃と防御の完全ガイド も合わせて読んでほしい。

導入手順と検証

1

DevTools の Network で主要レスポンスの Content-Type と X-Content-Type-Options の有無を確認する

2

HTML / JSON / 静的 JS・CSS / 画像に正しい MIME タイプが付いているか棚卸しする

3

404・500 エラーページにも nosniff が付くか確認する(nginx なら always)

4

CDN・nginx・Express ミドルウェアで nosniff を全レスポンスに付与する

5

ファイルアップロードがあれば許可 MIME・保存パス・attachment 方針を見直す

6

Mozilla Observatory や securityheaders.com でスコアと漏れを確認する

curl によるヘッダー確認

# トップページ
curl -sI https://example.com/ | grep -iE 'content-type|x-content-type'

# 404 ページ
curl -sI https://example.com/nonexistent-path-12345 | grep -iE 'content-type|x-content-type'

# 静的 JS
curl -sI https://example.com/assets/main.js | grep -i content-type

# API
curl -sI https://example.com/api/health | grep -iE 'content-type|x-content-type'

期待する出力例:

Content-Type: text/html; charset=utf-8
X-Content-Type-Options: nosniff

動作確認の具体例

  1. script ブロック確認Content-Type: text/plain.js を nosniff 付きで配信し、Console に MIME エラーが出ることを確認
  2. JSON APIapplication/json + nosniff で <script src="/api/user"> が実行されないこと
  3. アップロード再配信 — テスト用 HTML を text/plain で返す設定があれば、iframe 埋め込みで実行されないことを確認
  4. エラーページ — 意図的に 500 を発生させ、レスポンスヘッダーに nosniff があること

nosniff 導入で 既存の octet-stream script が壊れる場合がある。それは「もともと MIME が誤っていた」サインなので、正しい text/javascript へ直す。

Mozilla Observatory / securityheaders.com

外部スキャナーは次をチェックする。

  • X-Content-Type-Options: nosniff の有無
  • トップページだけでなくリダイレクト先
  • HTTPS サイトでの HSTS(別項目)

スコアだけを追うのではなく、レポートに載る URL 一覧を手動で spot check する。サブドメインや API ホストが別サーバーだと、メインドメインだけ A 評価でも実は漏れがあるパターンが多い。

nosniff だけでは足りないケース

脅威nosniff の効き必要な別対策
反射型・ stored XSSMIME 誤認部分のみ出力エスケープ、CSP、テンプレートの auto-escape
クリックジャッキング効かないframe-ancestors / X-Frame-Options
MITM・HTTP 混在効かないHSTS、HTTPS リダイレクト
Cookie 窃取間接的Secure、HttpOnly、SameSite
アップロード malware効かないウイルススキャン、実行権限のない保存先
SVG 内蔵 script限定的アップロード拒否、サニタイズ、CSP
PDF 内蔵 JS限定的PDF サニタイズ、viewer 設定
監査・コンプライアンス

PCI DSS、各種セキュリティベンチマーク、企業のベンダー監査では「セキュリティヘッダーが設定されているか」がチェック項目になる。nosniff は実装コストが最小クラスで、指摘対応の ROI が高い。

よくある実装ミスと対処

ミス症状対処
nginx で always 忘れ404/500 に nosniff がないadd_header ... always
子 location でヘッダー消滅一部パスだけスキャン不合格子 location でも再宣言
S3 の Content-Type 未設定octet-stream 配信アップロード時メタデータ指定
express.static の .mjstext/plain になるmime 拡張または CDN で修正
API だけヘッダー漏れ/api/* だけ不合格プロキシ or ミドルウェアで統一
ステージング未設定本番と挙動不一致IaC で全環境同一ヘッダー

関連記事

この記事は セキュリティ テーマの一環です。あわせて読むと理解が深まる関連記事をまとめました。

トピック記事
CSRF対策の実装ガイドCSRF対策の実装ガイド|SameSite・Synchronizer Token・Laravel/Expressコード
Double Submit Cookie完全実装Double Submit Cookie完全実装|Synchronizer Token比較・SPA+JWT・サブドメイン攻撃
CSP Report-Only 段階導入ガイドCSP Report-Only 段階導入ガイド|本番を壊さずポリシーを検証する
セキュアなCookie設定ガイドセキュアなCookie設定ガイド|HttpOnly・Secure・SameSiteの使い分けとCSRF対策
CORSエラーの対処法CORSエラーの対処法|「Access-Control-Allow-Origin」で解決する
CORSプリフライトリクエストCORSプリフライトリクエスト|OPTIONSが飛ぶ条件と正しい応答
ローカル開発でCORSエラーが出る原因と3つの回避策ローカル開発でCORSエラーが出る原因と3つの回避策
OAuth2 PKCE SPA実装ガイドOAuth2 PKCE SPA実装ガイド|Authorization Code + S256 + BFFパターン
Passkeys・WebAuthn実装の基礎Passkeys・WebAuthn実装の基礎|FIDO2・Resident Key・@simplewebauthn/server
JWTのデバッグ方法JWTのデバッグ方法|トークンの中身を安全に確認する
JWTの中身を確認する方法JWTの中身を確認する方法|改ざんが検知される仕組み
JWKSとJWT鍵ローテーション運用ガイドJWKSとJWT鍵ローテーション運用ガイド|kid検証・ゼロダウンタイム・Node.js実装

まとめ

MIME スニッフィングは Internet Explorer 時代から知られる攻撃面だが、Content-Type の不一致ユーザー生成ファイルの再配信という形で今も残る。X-Content-Type-Options: nosniff は一行で効く防御として、正しい Content-Type・multipart の安全なパース・ファイルアップロード設計とセットで導入する。

実務では次の3点をセットに覚えるとよい。

  1. 全レスポンス(エラー含む)に X-Content-Type-Options: nosniff
  2. 種類ごとの Content-Type 表に沿った MIME と charset
  3. セキュリティヘッダー一覧 の CSP・HSTS 等との多層防御

まず本番のレスポンスヘッダーを DevTools で1本確認し、nosniff がなければ nginx または Express で今日中に足す——それが最短の改善だ。