Chapter 70: Permissions Policy & Modern Security Headers

Permissions Policy HTTP Header Syntax

Structured Field Values (RFC 8941), allowlist grammar, origin serialization, and directive composition.

LEARNING OBJECTIVES
  • Master the RFC 8941 / RFC 9651 Structured Field Values grammar underpinning Permissions Policy.
  • Correctly construct allowlist tokens: () (disable), * (wildcard), self, and explicit quoted origin strings.
  • Combine multiple capability directives into a comma-delimited HTTP response header string.
  • Understand the exact parsing rules for origin matching, scheme mismatches, and subdomain boundaries.
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

Think of a traditional security guard checking credentials at an international airport terminal. If the instructions given to the guard are written in ambiguous, unstructured prose—such as "Only let staff through, or maybe airlines from Paris, but no one without a badge except on Tuesdays"—the guard will inevitably misinterpret the instructions, creating security holes.

To prevent this ambiguity, modern airports use a standardized electronic credential database where every rule is strictly typed:

Zone: Terminal-B-Lounge
Authorized-Entities: (Self, "https://partner-airlines.com")
Emergency-Access: False
+-------------------------------------------------------------------------------+
| OLD FEATURE POLICY (Whitespace-delimited, Fragile, Custom Parser)             |
| Feature-Policy: camera 'none'; microphone 'self' https://partner.com;         |
|                                                                               |
| NEW PERMISSIONS POLICY (RFC 8941 Structured Fields Dictionary)                |
| Permissions-Policy: camera=(), microphone=(self "https://partner.com")        |
+-------------------------------------------------------------------------------+

The IETF standardized this structured approach in RFC 8941: Structured Field Values for HTTP. The Permissions Policy HTTP header is formatted as a Structured Dictionary, where:

  1. Each key is a standardized feature name (e.g., camera, geolocation, payment).
  2. Each value is an allowlist expressed as an ordered list of origin tokens or strings enclosed in parentheses.

Because every modern browser uses the exact same RFC 8941 parsing engine, there is zero ambiguity, zero whitespace parsing errors, and consistent enforcement across Chrome, Firefox, Safari, and Edge.


Technical Deep Dive & Specifications

The RFC 8941 Structured Field Dictionary Grammar

Under the Permissions Policy specification, the HTTP response header value is parsed as a dictionary:

Permissions-Policy: <directive-1>=<allowlist>, <directive-2>=<allowlist>, ...

Each directive consists of a feature identifier followed by = and an allowlist definition.

                  Permissions-Policy Header Structure
                  
  Permissions-Policy: camera=(self), geolocation=(), payment=(self "https://pay.stripe.com")
                      \____/\_____/  \_________/\__/  \_____/\_____________________________/
                         |     |          |       |      |                   |
                     Feature Allowlist Feature Allowlist Feature          Allowlist
                      Name   (Self)     Name   (Disabled) Name   (Self + Explicit Origin)

Allowlist Target Identifiers

The allowlist determines which origins are permitted to execute the feature. The table below illustrates the four primary allowlist targets:

Allowlist Target Syntax Example Technical Meaning & Origin Scope
Disable Completely feature=() The feature is disabled for all origins, including the top-level origin itself and all embedded <iframe> elements.
Origin Self Only feature=(self) or feature=self The feature is allowed only for the document's own origin (identical scheme, host, and port). All cross-origin frames are blocked by default.
Universal Wildcard feature=* The feature is allowed for the top-level origin and any cross-origin iframe loaded in the document tree.
Explicit Origin List feature=(self "https://example.com") The feature is allowed for self and explicitly listed origin string literals. Explicit origins must be enclosed in double quotes.

Detailed Syntax Matrix & Common Misconfigurations

Intent ✅ Correct Permissions-Policy Header ❌ Incorrect / Legacy / Broken Syntax
Disable Camera Permissions-Policy: camera=() Permissions-Policy: camera='none' (Legacy syntax)
Allow Self for Mic Permissions-Policy: microphone=(self) Permissions-Policy: microphone='self' (Invalid quotes)
Allow Multiple Origins Permissions-Policy: geolocation=(self "https://maps.google.com") Permissions-Policy: geolocation=(self, https://maps.google.com) (Unquoted origin & comma error)
Allow All Frames for Fullscreen Permissions-Policy: fullscreen=* Permissions-Policy: fullscreen=(*) or fullscreen='*'
Combine Multiple Directives Permissions-Policy: camera=(), microphone=(), payment=(self) Permissions-Policy: camera=(); microphone=(); payment=(self) (Semicolons are invalid)

Origin Matching & Normalization Rules

When the browser evaluates whether an origin matches an entry in a Permissions Policy allowlist, it applies strict Same-Origin normalization:

Allowlist Entry: "https://auth.example.com"
Target Origin:   "https://auth.example.com:443"  ===> ✅ MATCH (Default HTTPS port 443)
Target Origin:   "http://auth.example.com"       ===> ❌ REJECT (Scheme mismatch: HTTP != HTTPS)
Target Origin:   "https://api.example.com"        ===> ❌ REJECT (Subdomain mismatch)
Target Origin:   "https://auth.example.com:8443"  ===> ❌ REJECT (Port mismatch)
  1. Origin Strings Must Be Canonical: You cannot specify pathnames, query parameters, or fragments.
    • "https://example.com"
    • "https://example.com/checkout" (Invalid: contains path)
    • "https://example.com?v=1" (Invalid: contains query)
  2. Wildcards in Hostnames Are Forbidden:
    • "https://*.example.com" (Wildcard subdomains are not permitted by RFC 8941 grammar; every subdomain must be explicitly enumerated).

💻 Interactive Code Playground

Starter Code

Save the following complete Node.js / Express server as server.js. This demonstrates how to construct, serve, and dynamically validate RFC 8941 Permissions-Policy headers across different API routes.

Line-by-Line Code Breakdown

  • Lines 10–22: Builds a structured array of directives. Features that must be completely blocked (camera, microphone, usb) use empty parentheses ().
  • Lines 12–13: Features allowed for self and specific partner origins use (self "https://..."). Notice the origin is enclosed in double quotes.
  • Lines 14: fullscreen=* uses the wildcard token to allow full-screen video in any embedded frame.
  • Line 22 (join(', ')): Joins all directives with commas according to the RFC 8941 dictionary grammar.
  • Lines 63–76: The client-side script probes document.permissionsPolicy.allowsFeature() and verifies that the live engine enforcement matches the server's policy.

Expected Browser Render Output


/**
 * Permissions Policy Demonstration Server
 * Run via: node server.js
 */
const http = require('http');

const server = http.createServer((req, res) => {
  // Define strict Permissions-Policy header using RFC 8941 Structured Fields
  const permissionsPolicy = [
    'camera=()',
    'microphone=()',
    'geolocation=(self "https://trusted-maps.example.com")',
    'payment=(self "https://checkout.stripe.com")',
    'fullscreen=*',
    'display-capture=()',
    'accelerometer=()',
    'gyroscope=()',
    'magnetometer=()',
    'usb=()',
    'serial=()'
  ].join(', ');

  // Set standard security headers
  res.writeHead(200, {
    'Content-Type': 'text/html; charset=UTF-8',
    'Permissions-Policy': permissionsPolicy,
    'X-Content-Type-Options': 'nosniff'
  });

  const html = `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Permissions Policy Syntax Testbed</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 2rem; }
    .card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1.5rem; max-width: 800px; margin: 0 auto; }
    pre { background: #090d16; color: #38bdf8; padding: 1rem; border-radius: 6px; overflow-x: auto; font-size: 0.9rem; }
    .token-keyword { color: #f43f5e; font-weight: bold; }
    .token-origin { color: #10b981; }
    .token-feature { color: #fbbf24; }
  </style>
</head>
<body>
  <div class="card">
    <h1>🛡️ Active Permissions Policy Header</h1>
    <p>The following RFC 8941 Structured Header was delivered in this HTTP response:</p>
    <pre>${permissionsPolicy}</pre>

    <h2>Live Capability Probe</h2>
    <p>Checking browser enforcement of declared allowlists:</p>
    <ul id="results"></ul>
  </div>

  <script>
    const results = document.getElementById('results');
    const tests = [
      { name: 'camera', expected: false },
      { name: 'microphone', expected: false },
      { name: 'geolocation', expected: true },
      { name: 'fullscreen', expected: true },
      { name: 'usb', expected: false }
    ];

    tests.forEach(test => {
      let allowed = false;
      if (document.permissionsPolicy && document.permissionsPolicy.allowsFeature) {
        allowed = document.permissionsPolicy.allowsFeature(test.name);
      }
      const li = document.createElement('li');
      const passed = allowed === test.expected;
      li.innerHTML = \`<strong>\${test.name}:</strong> \${allowed ? 'ALLOWED' : 'BLOCKED'} — \` +
                     \`<span style="color: \${passed ? '#10b981' : '#ef4444'}">\${passed ? 'PASS (Matches Policy)' : 'FAIL'}</span>\`;
      results.appendChild(li);
    });
  </script>
</body>
</html>`;

  res.end(html);
});

server.listen(3000, () => {
  console.log('Testbed running at http://localhost:3000');
});
🛡️ Active Permissions Policy Header
-----------------------------------------------------------------------------
camera=(), microphone=(), geolocation=(self "https://trusted-maps.example.com"), payment=(self "https://checkout.stripe.com"), fullscreen=*, display-capture=(), accelerometer=(), gyroscope=(), magnetometer=(), usb=(), serial=()

Live Capability Probe:
• camera: BLOCKED — PASS (Matches Policy)
• microphone: BLOCKED — PASS (Matches Policy)
• geolocation: ALLOWED — PASS (Matches Policy)
• fullscreen: ALLOWED — PASS (Matches Policy)
• usb: BLOCKED — PASS (Matches Policy)

🏋️ Hands-On Exercise

🎯 The Challenge: Build a Permissions-Policy Header Builder & Validator

Scenario: You are building an internal CLI tool for your infrastructure team. The tool takes an object representing desired feature permissions and generates a syntactically valid RFC 8941 Permissions-Policy header. If a developer attempts to use invalid tokens (like 'none', 'self', unquoted URLs, or wildcard subdomains), your validator must throw a descriptive syntax error.

Instructions:

  1. Write a function generatePermissionsPolicy(config) where config maps feature names to permission rules ('none', 'self', '*', or array of origins).
  2. Transform 'none' to ().
  3. Transform 'self' to (self).
  4. Transform '*' to *.
  5. Transform arrays of origins (e.g. ['self', 'https://api.stripe.com']) to (self "https://api.stripe.com").
  6. Validate that any custom origin begins with https:// and does not contain wildcard domains *..

🏁 Starter Code Sandbox

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

⚠️ Common Pitfalls

  1. Using Commas Inside the Allowlist Parentheses: Writing payment=(self, "https://stripe.com") is invalid. Inside the parentheses, origins must be space-separated (e.g., payment=(self "https://stripe.com")). Directives themselves are comma-separated.
  2. Wrapping self in Quotes: Writing camera=("self") treats "self" as an origin string named https://self instead of the keyword token self. Write camera=(self).
  3. Using Semicolons as Directive Separators: Semicolons were used in the deprecated Feature-Policy header. Permissions-Policy uses commas , as dictionary entry separators.

💡 Pro Tips

  1. Automate Header Generation via Middleware: In Node.js or Next.js, never write long Permissions-Policy header strings by hand. Use a typed configuration object that compiles to RFC 8941 at build time.
  2. Split Across Multiple Lines with HTTP Header Folding (HTTP/2 / HTTP/3): For servers emitting large lists, modern HTTP/2 and HTTP/3 multiplexed streams compress duplicate header keys via HPACK/QPACK, keeping byte overhead near zero.
  3. Pair (self) with Strict Subresource Integrity (SRI): Allowing (self) allows any script executing in the top-level origin. Ensure third-party scripts loaded into the top-level origin are signed with SRI to prevent supply-chain tampering.

📌 Key Takeaways

  • Permissions Policy uses IETF RFC 8941 Structured Field Values (dictionary of lists).
  • feature=() disables a capability across all origins and frames.
  • feature=(self) limits capability to the current origin only.
  • feature=* allows the capability in all top-level and embedded origins.
  • Explicit origins must be enclosed in double quotes (e.g., feature=(self "https://trusted.com")) and separated by spaces inside parentheses.
  • Directives are separated by commas , (not semicolons ;).
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

Which of the following headers represents a syntactically valid Permissions Policy that disables the microphone and allows geolocation for the current origin and Google Maps?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

What is wrong with the following header value: Permissions-Policy: camera=("self")?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

Are wildcard subdomains such as payment=("https://*.stripe.com") supported in Permissions Policy headers?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP