Permissions-Policy 完全ガイド|iframe・camera・microphone・geolocation・allow 属性

(更新: 2026年6月22日 ) Permissions-Policy セキュリティ HTTPヘッダー iframe Web nginx
結論
  • Permissions-Policy(旧 Feature-Policy)は、カメラ・マイク・位置情報・決済 API などブラウザ組み込み機能の利用上限を HTTP ヘッダーと <iframe allow> で宣言する仕組みだ。
  • 一般サイトは camera=(), microphone=(), geolocation=() で閉じ、必要なサードパーティ iframe だけ allow 属性とヘッダーの許可オリジンを一致させて委譲する。
  • CSP が「何を読み込むか」を制御するのに対し、Permissions-Policy は「何が実行できるか」を制御する——CSP 設定ガイドセキュリティヘッダー一覧と合わせて多層防御を完成させる。

Permissions-Policy とは

Permissions-Policy は、W3C の Permissions Policy 仕様に基づく HTTP レスポンスヘッダーだ。旧称は Feature-Policy。ブラウザが提供する強力な API——navigator.mediaDevices.getUserMedia()(カメラ・マイク)、navigator.geolocation.getCurrentPosition()(位置情報)、Payment Request API、WebUSB、フルスクリーン表示など——を、文書(document)単位および iframe 継承チェーン単位で有効・無効にできる。

2026 年時点の主要ブラウザ(Chrome、Firefox、Safari、Edge)は Permissions-Policy をサポートしている。セキュリティスキャナ(securityheaders.comMozilla Observatory)でも評価対象となり、セキュリティヘッダー一覧と設定ガイドで紹介している CSP・HSTS・Referrer-Policy と並ぶ実務標準の防御層だ。

CSP や X-Frame-Options との違い

混同しやすいが、役割はまったく異なる。

メカニズム 制御対象
Content-Security-Policy (CSP) script・style・img・connect などリソースの読み込み元。XSS やデータ流出経路の遮断が主目的([CSP 設定ガイド](/blog/csp-content-security-policy-設定ガイド/)参照)
X-Frame-Options / frame-ancestors このページを誰が iframe に埋め込めるか(クリックジャッキング対策)。埋め込み先の API 利用は制御しない
Permissions-Policy このページ(と子 iframe)がどのブラウザ API を呼べるか。カメラ・マイク・位置情報などデバイス/センサー系が主戦場
iframe allow 属性 親文書から特定の子 iframe へ権限を委譲する HTML レベルの宣言。ヘッダーと組み合わせて使う

たとえば EC サイトが Stripe の決済 iframe を埋め込む場合、CSP の frame-src https://js.stripe.com で iframe の読み込みを許可し、Permissions-Policy の payment=(self "https://js.stripe.com")<iframe allow="payment 'https://js.stripe.com'"> で Payment Request API の利用を委譲する——という二段構えになる。

Permissions API との関係

ブラウザの Permissions APInavigator.permissions.query())は、ユーザーがカメラ等を許可したかどうかを問い合わせる API だ。Permissions-Policy はその前段で「そもそもこの文書では機能が無効」とブラウザに指示する。ポリシーで camera=() なら、Permissions API の結果以前に getUserMedia() は拒否される。

構文と許可リストモデル

Permissions-Policy ヘッダーは、カンマ区切りで複数のポリシー宣言を並べる。

Permissions-Policy: camera=(), microphone=(), geolocation=()

各宣言は 機能名=(許可リスト) の形式だ。

許可リストの書き方意味
()空リスト = 全面禁止(最もよく使う「閉じる」指定)
(self)同一オリジンの文書のみ許可
(*)すべてのオリジンに許可(本番非推奨。攻撃面が最大化する)
("https://example.com")特定オリジンを明示許可(ダブルクォートで囲む)
(self "https://maps.google.com")自サイトと列挙したオリジンのみ

継承と委譲のルール

ブラウザは次のルールで権限を計算する。

  1. トップレベル文書 — レスポンスヘッダーの Permissions-Policy が上限になる
  2. 子 iframe — 親が持つ権限の部分集合しか持てない(親が禁止なら子も禁止)
  3. allow 属性 — 親が持つ権限のうち、子に渡したいものだけを列挙する
  4. クロスオリジン iframe — デフォルトでは強力な API はすべて禁止。明示的な委譲が必要
1

オリジン example.com が Permissions-Policy: camera=(self) を返す

2

同一オリジンの /settings/camera ページは getUserMedia() を呼べる(ユーザー許可は別途必要)

3

example.com 内の <iframe src="/embed/widget"> に allow="camera" を付けると子にも camera が渡る

4

example.com が <iframe src="https://vendor.example"> を埋め込む場合、ヘッダーに camera=(self "https://vendor.example") と allow="camera" の両方が必要

5

ヘッダーが camera=() なら allow を付けても vendor.example ではカメラは使えない

主要な機能名リファレンス

2026 年の実務で触ることが多い機能名を整理する。完全な一覧は MDN Permissions-Policy を参照。

機能名 制御対象と実務メモ
camera getUserMedia({ video: true })。ビデオ会議・本人確認・QR スキャンで必要。不要なら () で閉じる
microphone getUserMedia({ audio: true })。音声入力・通話。camera と独立して指定できる
geolocation navigator.geolocation。配送追跡・店舗検索・チェックイン。プライバシー影響大のためデフォルト閉鎖が増加傾向
payment Payment Request API。Stripe・PayPal 等の決済 iframe とセットで設定
fullscreen <video>.requestFullscreen() 等。動画プレイヤー埋め込み時に allow="fullscreen" が必要なことが多い
usb WebUSB。IoT 管理画面以外では () 推奨
accelerometer / gyroscope / magnetometer デバイスモーション・コンパス系。モバイル WebGL ゲーム以外は閉じてよい
display-capture 画面共有(getDisplayMedia)。リモートサポート SaaS 向け
clipboard-read / clipboard-write クリップボード API。ブラウザ差が大きいため、必要時のみ (self) で開放

camera・microphone・geolocation の深掘り

camera と microphone

navigator.mediaDevices.getUserMedia() は、HTTPS 配信かつユーザー操作をきっかけに権限ダイアログを出す。XSS で悪意ある script が実行されると、ポリシーが緩い場合はユーザーを騙してカメラ起動を試みられる。

推奨デフォルト(API を使わない一般サイト):

Permissions-Policy: camera=(), microphone=(), display-capture=()

自社オリジンのみビデオチャットを提供する SaaS:

Permissions-Policy: camera=(self), microphone=(self), display-capture=(self), geolocation=()

同一オリジン内の /room/abc ページで WebRTC を動かす場合でも、別オリジンの広告 iframe には camera が渡らない——これが Permissions-Policy の要点だ。

geolocation

位置情報は一度許可されると精度の高い座標が取得できる。フィッシングページに埋め込まれた iframe 経由で位置が漏れるケースを防ぐため、2020 年代以降「デフォルトで geolocation=()」するサイトが増えている。

位置情報を自ページの JS だけで使う場合:

Permissions-Policy: geolocation=(self), camera=(), microphone=()

Google Maps Embed API を iframe で使う場合(ヘッダー側):

Permissions-Policy: geolocation=(self "https://maps.google.com"), camera=(), microphone=()

対応する HTML:

<iframe
  src="https://maps.google.com/maps/embed?pb=..."
  width="600"
  height="450"
  style="border:0;"
  allow="geolocation 'https://maps.google.com'"
  referrerpolicy="no-referrer-when-downgrade"
  loading="lazy"
  title="店舗地図"
></iframe>

allow 属性の値は、スペース区切りで機能名とオリジンを列挙する。機能名だけ書くと、その iframe のオリジン(src のオリジン)への委譲を意味する。クロスオリジンで特定ドメインを指定するときは geolocation 'https://maps.google.com' のようにクォート付きオリジンを書く。

geolocation の二重チェック

ヘッダーで geolocation=(self "https://maps.google.com") と書いても、iframe に allow="geolocation" を付け忘れると埋め込み地図は位置連動できない。逆に allow だけ付けてヘッダーが geolocation=() なら動かない。ヘッダーと allow はセットで設計する。

iframe の allow 属性

HTML Living Standard では、<iframe>allow 属性が Permissions Policy の委譲リストを表す。ヘッダーがサイト全体の上限を決め、allow が「この iframe だけ例外的に渡す」調整弁になる。

allow 属性の構文

allow = "feature origin origin ..."
記述例意味
allow="camera; microphone"この iframe のオリジンに camera と microphone を委譲
allow="camera 'https://meet.example.com'"meet.example.com オリジン向けに camera を委譲
allow="payment 'https://js.stripe.com'; fullscreen"Stripe に payment、自 iframe オリジンに fullscreen
allow="none"明示的にすべて委譲しない(子のデフォルト禁止を維持)

セミコロン ; はカンマ同様に区切りとして使える。複数機能を並べるときは読みやすさのためセミコロン区切りが一般的だ。

よくある埋め込みパターン

YouTube 動画(fullscreen + autoplay):

<iframe
  src="https://www.youtube.com/embed/dQw4w9WgXcQ"
  width="560"
  height="315"
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen"
  allowfullscreen
  title="製品デモ動画"
></iframe>

Stripe Checkout 埋め込み:

<iframe
  src="https://js.stripe.com/v3/checkout"
  allow="payment 'https://js.stripe.com'"
  sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
  title="お支払い"
></iframe>

自社ドメインの WebRTC ウィジェット(同一サイト内 iframe):

<iframe
  src="/widgets/video-call"
  allow="camera; microphone; display-capture"
  title="ビデオ通話"
></iframe>

親ページのヘッダーが camera=(self), microphone=(self) であることが前提だ。親が camera=() なら上記 allow は無効になる。

完全設定例(5 パターン)

以下はすべて省略なしのコピー可能な設定だ。ドメイン・証明書パスは環境に合わせて置き換える。

設定例 1: 一般コーポレートサイト(API 全面禁止)

ブログ・ランディングページ・ドキュメントサイト向け。カメラ・マイク・位置情報・決済・USB をすべて閉じる。

HTTP レスポンスヘッダー(1 行):

Permissions-Policy: accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()

nginx add_header:

add_header Permissions-Policy "accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" always;

設定例 2: WebRTC ビデオ会議 SaaS(自オリジンのみ許可)

Permissions-Policy: camera=(self), microphone=(self), display-capture=(self), geolocation=(), payment=(), usb=()

トップページのヘッダーは上記のとおり。会議ルーム /call は同一オリジンなので getUserMedia がポリシー上許可される。第三者のチャットウィジェットを iframe 埋め込みする場合は、そのベンダーオリジンを個別に追加する。

設定例 3: EC サイト(Stripe 決済 iframe)

Permissions-Policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(self "https://js.stripe.com"), usb=()

CSP 側では frame-src https://js.stripe.comconnect-src https://api.stripe.com も必要(CSP 設定ガイドの frame-src / connect-src を参照)。Permissions-Policy は payment 機能の委譲のみを担う。

設定例 4: 店舗検索サイト(Google 地図 iframe + 自前 geolocation)

Permissions-Policy: camera=(), microphone=(), geolocation=(self "https://maps.google.com"), payment=(), usb=()

自社 JS が navigator.geolocation を使う一覧ページと、Google Maps 埋め込みの詳細ページが共存するケース。一覧は (self) だけで足りるが、地図 iframe には "https://maps.google.com" を追加する。

設定例 5: カスタマーサポート(画面共有ベンダー iframe)

Permissions-Policy: camera=(self), microphone=(self), display-capture=(self "https://support.vendor.example"), geolocation=(), payment=()
<iframe
  src="https://support.vendor.example/session/abc123"
  allow="camera; microphone; display-capture 'https://support.vendor.example'"
  sandbox="allow-scripts allow-same-origin allow-forms"
  title="リモートサポートセッション"
></iframe>

sandbox 属性は Permissions-Policy とは別系統の制限だ。ベンダー仕様に従い、必要最小限の sandbox トークンだけ付与する。

nginx 完全設定例

セキュリティヘッダー一覧の nginx 例に、Permissions-Policy を組み込んだ単体サーバーブロックだ。

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name app.example.com;

    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    root /var/www/app.example.com/dist;
    index index.html;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-src 'self' https://js.stripe.com https://maps.google.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests" always;
    add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(self \"https://maps.google.com\"), gyroscope=(), magnetometer=(), microphone=(), payment=(self \"https://js.stripe.com\"), usb=()" always;
    add_header Cross-Origin-Opener-Policy "same-origin" always;

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff2?)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(self \"https://maps.google.com\"), gyroscope=(), magnetometer=(), microphone=(), payment=(self \"https://js.stripe.com\"), usb=()" always;
        try_files $uri =404;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()" always;
    }

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

nginx では location 内で add_header を書くと server レベルの add_header が上書きされるため、静的ファイル用 location でも Permissions-Policy を再宣言している。API プロキシでは geolocation を閉じ、フロント用のデフォルトより厳しくする例も示した。

Express ミドルウェア例

Node.js / Express で HTML を配信する場合、ミドルウェアで一律付与できる。

// middleware/permissionsPolicy.js
const CORPORATE_DEFAULT =
  'accelerometer=(), autoplay=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()';

const CHECKOUT_PAGE =
  'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(self "https://js.stripe.com"), usb=()';

function permissionsPolicy(req, res, next) {
  const isCheckout = req.path.startsWith('/checkout');
  res.setHeader('Permissions-Policy', isCheckout ? CHECKOUT_PAGE : CORPORATE_DEFAULT);
  next();
}

module.exports = { permissionsPolicy };
// app.js
const express = require('express');
const { permissionsPolicy } = require('./middleware/permissionsPolicy');

const app = express();

app.use(permissionsPolicy);

app.get('/checkout', (req, res) => {
  res.type('html').send(`<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <title>お支払い</title>
</head>
<body>
  <h1>お支払い</h1>
  <iframe
    src="https://js.stripe.com/v3/checkout"
    allow="payment 'https://js.stripe.com'"
    title="Stripe Checkout"
    width="100%"
    height="600"
  ></iframe>
</body>
</html>`);
});

app.listen(3000);

パスごとにポリシーを変える場合は、ルーター単位でミドルウェアを差し替えるか、上記のように req.path で分岐する。

Cloudflare Transform Rules 例

CDN でヘッダーを一元管理するチーム向け。Cloudflare ダッシュボードの Rules → Transform Rules → Modify response header で次を設定する。

設定項目
Rule nameAdd Permissions-Policy default
Whenhttp.host eq "www.example.com"
ThenSet static header Permissions-Policy

ヘッダー値:

accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()

オリジン(nginx)と Cloudflare の両方で同じヘッダーを付けると二重になる。どちらか一方で管理し、セキュリティヘッダー一覧の「CDN と nginx の使い分け」に従って重複を避ける。

導入フローとテスト手順

1

本番相当環境で curl -sI https://app.example.com | grep -i permissions-policy を実行し、現状の有無を確認する

2

カメラ・マイク・位置情報を使うページとサードパーティ iframe の一覧をスプレッドシートにまとめる

3

まずステージングで camera=(), microphone=(), geolocation=() の厳しめポリシーを適用する

4

各 iframe の Console に Permissions Policy violation が出ないか確認し、必要な allow とヘッダー許可オリジンを追加する

5

決済・地図・動画の E2E テスト(Playwright 等)を通す

6

本番反映後、securityheaders.com で Permissions-Policy の存在を確認する

DevTools での確認

  1. Network タブで HTML レスポンスの Permissions-Policy ヘッダーを確認
  2. ConsolegetUserMediageolocation を実行し、拒否メッセージを読む
  3. 拒否時の典型メッセージ: Permission policy 'camera' check failed for document with origin 'https://evil.example'

プログラムでの自己診断

async function diagnosePermissionsPolicy() {
  const features = ['camera', 'microphone', 'geolocation', 'payment'];
  for (const name of features) {
    try {
      const status = await navigator.permissions.query({ name });
      console.log(name, status.state);
    } catch (err) {
      console.log(name, 'not supported or blocked by policy', err.message);
    }
  }
}

permissions.query() はブラウザ・機能によっては例外になる。ポリシーで禁止された機能は、API 呼び出しそのものが NotAllowedErrorSecurityError になることが多い。

よくある失敗と対処

症状原因対処
Stripe 決済 iframe が初期化しないpayment() のままヘッダーに payment=(self "https://js.stripe.com") と iframe の allow="payment 'https://js.stripe.com'" を追加
地図が「位置情報を有効にしてください」と出るgeolocation のオリジン漏れヘッダーと allow の両方に https://maps.google.com を追加
自社 WebRTC は動くがベンダー iframe だけ動かないベンダーオリジンが許可リストに無いcamera=(self "https://vendor.example") のように第三者を列挙
設定したのにヘッダーが付かないnginx の location で add_header が消えた該当 location に同じ add_header を再宣言
Report-Only が欲しいPermissions-Policy に Report-Only は無い仕様上、本番は即時強制。ステージングで検証してから本番へ

Permissions-Policy には CSP のような Report-Only モードは存在しない。ポリシーを返した瞬間からブラウザが強制する。だからこそステージングでの回帰テストが重要だ。

Document-Policy ヘッダーとの混同に注意

Document-Policy は別ヘッダーで、JS の document.domain 廃止方向の機能制御などに使われる。Permissions-Policy とは名前が似ているが無関係。設定画面で間違えないこと。

セキュリティヘッダーとの統合

セキュリティヘッダー一覧と設定ガイドで推奨している構成に Permissions-Policy を足すと、次のような多層防御になる。

ヘッダーPermissions-Policy との関係
トランスポートHSTS無関係だが、getUserMedia は Secure Context(HTTPS)必須のため事実上セットで必要
リソース制御CSPiframe の srcframe-src で制限。Permissions-Policy は埋め込み先の API 利用を制限
埋め込み制御frame-ancestors自サイトが他人に iframe されるのを防ぐ。子 iframe への委譲とは別
デバイス APIPermissions-Policy本記事の主題
リファラーReferrer-Policy位置情報とは独立だが、プライバシー設計では一緒に見直す

CSP で frame-src を厳しくし、Permissions-Policy で camera=() にする——この組み合わせは、悪意ある第三者 iframe の読み込みと、万一読み込まれた場合のカメラ起動の両方を抑える。

2026 年のトレンドとベストプラクティス

  1. デフォルト閉鎖 — camera / microphone / geolocation は明示的な要件がない限り () から始める
  2. オリジン列挙* は使わず、Stripe・Google Maps 等、実在する embed オリジンだけをクォート付きで追加
  3. allow 属性の HTML リント — ESLint プラグインやカスタム CI で、iframe に必要な allow が付いているか静的チェック
  4. サードパーティ Cookie 廃止との並走 — iframe 内 API の制限は Cookie 制限と同時期に問題化しやすい。ベンダー文档の「required permissions」セクションを必ず読む
  5. プライバシー規制 — 位置情報・マイクは GDPR・個人情報保護法の観点でも説明責任が重い。ポリシーで閉じ、UI で明示的オプトインする

Feature-Policy からの移行

2018 年頃に普及した Feature-Policy ヘッダーは、構文が camera 'self' のように括弧を使わない形式だった。現在の Permissions-Policy は camera=(self)括弧付き許可リストが正式構文だ。

旧 Feature-Policy(非推奨)現行 Permissions-Policy
camera 'none'camera=()
camera 'self'camera=(self)
camera *camera=(*)(本番非推奨)
camera 'self' https://a.examplecamera=(self "https://a.example")

移行期にレガシーブラウザを気にする場合のみ、同一値を両ヘッダー名で返す手法がある。

Feature-Policy: camera 'none'; microphone 'none'; geolocation 'none'
Permissions-Policy: camera=(), microphone=(), geolocation=()

2026 年の新規プロジェクトでは Permissions-Policy のみで十分だ。Feature-Policy を残すと、設定ドリフト(片方だけ更新)の温床になる。

Next.js / Vercel の headers 設定

Next.js の next.config.js で全ページにヘッダーを付与できる。App Router・Pages Router どちらでも headers() 関数は同じ。

/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'Permissions-Policy',
            value:
              'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()',
          },
        ],
      },
      {
        source: '/checkout/:path*',
        headers: [
          {
            key: 'Permissions-Policy',
            value:
              'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(self "https://js.stripe.com"), usb=()',
          },
        ],
      },
    ];
  },
};

module.exports = nextConfig;

vercel.json で管理する場合:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Permissions-Policy",
          "value": "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
        }
      ]
    }
  ]
}

CSP も同じファイルで設定する場合は、CSP 設定ガイドの Next.js 例と併記し、frame-src と payment 許可オリジンの整合を取る。

Apache mod_headers 設定例

nginx を使わない LAMP / XAMPP 環境では mod_headers で付与する。

<VirtualHost *:443>
    ServerName app.example.com
    DocumentRoot /var/www/app.example.com/public

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/app.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/app.example.com/privkey.pem

    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"

    <Location /checkout>
        Header always set Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(self \"https://js.stripe.com\"), usb=()"
    </Location>
</VirtualHost>

Header always setalways は、4xx / 5xx レスポンスにもヘッダーを付ける nginx の always フラグと同趣旨だ。

Passkeys と publickey-credentials-get

WebAuthn / Passkeys を自サイトで使う場合、publickey-credentials-get 機能の許可が必要になることがある。

Permissions-Policy: publickey-credentials-get=(self), camera=(), microphone=(), geolocation=()

Passkeys 実装の背景は Passkeys / WebAuthn 実装の基礎 を参照。認証 iframe を第三者がホストする構成では、そのオリジンを許可リストに追加する。

Playwright による回帰テスト

ヘッダーが期待どおり付与されているか、CI で自動検証できる。

// tests/permissions-policy.spec.js
const { test, expect } = require('@playwright/test');

test('トップページは camera を禁止する Permissions-Policy を返す', async ({ request }) => {
  const response = await request.get('https://staging.example.com/');
  const policy = response.headers()['permissions-policy'];
  expect(policy).toBeDefined();
  expect(policy).toContain('camera=()');
  expect(policy).toContain('microphone=()');
  expect(policy).toContain('geolocation=()');
});

test('checkout は Stripe 向け payment を許可する', async ({ request }) => {
  const response = await request.get('https://staging.example.com/checkout');
  const policy = response.headers()['permissions-policy'];
  expect(policy).toContain('payment=(self "https://js.stripe.com")');
});

ブラウザコンテキストで getUserMedia の拒否を E2E する場合は、ヘッドフル実行と仮想メディアデバイスの指定が必要になる。まずは HTTP ヘッダーの存在と文字列一致から始めるのが現実的だ。

サイト種別別クイックリファレンス

サイト種別推奨 Permissions-Policy(要約)iframe allow の要点
コーポレート / ブログcamera=(), microphone=(), geolocation=(), payment=()動画埋め込みのみ allow="fullscreen"
EC / 決済上記 + payment=(self "https://js.stripe.com")allow="payment 'https://js.stripe.com'"
地図・配送geolocation=(self) または Maps オリジン追加地図 iframe に allow="geolocation 'https://maps.google.com'"
ビデオ会議camera=(self), microphone=(self), display-capture=(self)自社 widget iframe に allow="camera; microphone"
管理画面(高セキュリティ)上記に加え usb=(), bluetooth=(), serial=() 等も閉鎖サードパーティ iframe 自体を CSP frame-src で禁止

攻撃シナリオと防御の対応

Permissions-Policy 単体では XSS や CSRF は防げない。しかし、XSS で注入された script がデバイス API を叩く影響範囲を縮小できる。

シナリオ A: 悪意ある広告 iframe

攻撃者が広告ネットワーク経由で <iframe src="https://evil.ads"> を表示し、子 frame で getUserMedia を試みる。親が camera=() なら子は呼べない。CSP の frame-src で広告ドメイン自体を許可していなければ iframe すら読み込まれない——CSP 設定ガイドframe-src と二重で効く。

シナリオ B: 同一オリジン XSS

自サイトに XSS があり、同一オリジンなので camera=(self) ではブロックされない。対策は CSP による script 実行阻止が本命(XSS 完全ガイド)。Permissions-Policy は「第三者 iframe 経由の濫用」を主に想定した層だと理解する。

シナリオ C: クリックジャッキング + 位置情報

透明 iframe でボタンを覆い、ユーザー操作と同時に geolocation を取得しようとする。geolocation=() かつ frame-ancestors 'none'(CSP)で、埋め込みと位置取得の両方を抑える。

同一オリジン XSS では camera=(self) は無力

「Permissions-Policy を付けたからカメラは安全」は誤解だ。自社オリジンで動く悪意ある script には self 許可がそのまま効く。CSP・出力エスケープ・サニタイズを優先し、Permissions-Policy は深層防御の一層として位置づける。

まとめ
  • Permissions-Policy は「使わない強力 API を閉じ、必要な iframe だけに最小限委譲する」ためのヘッダーだ。
  • camera=(), microphone=(), geolocation=() をベースに、決済・地図・通話の要件に応じてオリジンを足す。
  • iframe の allow 属性は忘れずにペアで設計し、CSP・frame-ancestors と役割分担させる。
  • 詳細な nginx 一式は セキュリティヘッダー一覧、スクリプト読み込み制御は CSP 設定ガイド を参照してほしい。

関連記事

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

トピック記事
CSRF対策の実装ガイドCSRF対策の実装ガイド|SameSite・Synchronizer Token・Laravel/Expressコード
Double Submit Cookie完全実装Double Submit Cookie完全実装|Synchronizer Token比較・SPA+JWT・サブドメイン攻撃
セキュアなCookie設定ガイドセキュアなCookie設定ガイド|HttpOnly・Secure・SameSiteの使い分けとCSRF対策
CORSエラーの対処法CORSエラーの対処法|「Access-Control-Allow-Origin」で解決する
ローカル開発でCORSエラーが出る原因と3つの回避策ローカル開発でCORSエラーが出る原因と3つの回避策
OAuth2 PKCE SPA実装ガイドOAuth2 PKCE SPA実装ガイド|Authorization Code + S256 + BFFパターン
JWTのデバッグ方法JWTのデバッグ方法|トークンの中身を安全に確認する
JWTの中身を確認する方法JWTの中身を確認する方法|改ざんが検知される仕組み
JWKSとJWT鍵ローテーション運用ガイドJWKSとJWT鍵ローテーション運用ガイド|kid検証・ゼロダウンタイム・Node.js実装
HSTS Preload設定手順HSTS Preload設定手順|nginx・Cloudflare実装とhstspreload.org申請ガイド
X-Content-Type-Options: nosniff 解説X-Content-Type-Options: nosniff 解説|MIMEスニッフィング対策
Subresource Integrity (SRI) 完全ガイドSubresource Integrity (SRI) 完全ガイド|CDN スクリプトの integrity・crossorigin・フォールバック