Skip to content

Permissions-Policy Header

Status: ✅ Implemented (2026-01-06) Priority: Medium Effort: 30 minutes Category: Security Enhancement

Overview

The Permissions-Policy header (formerly Feature-Policy) restricts browser features and prevents unauthorized access to sensitive device APIs. This is a defense-in-depth security measure that complements other security controls.

Problem Statement

Prior to this enhancement:

  • No Permissions-Policy header was configured
  • Browser features like camera, microphone, geolocation were not explicitly disabled
  • Potential attack surface for side-channel attacks via sensors
  • Missing defense-in-depth layer recommended by security best practices

From GAP_ANALYSIS.md section 9.1:

The Permissions-Policy header is not configured, leaving browser features unrestricted. While Seed doesn't use these features, explicitly disabling them provides defense-in-depth protection against potential vulnerabilities.

Solution

Implement custom middleware to add the Permissions-Policy header that disables all unnecessary browser features. Since Seed is an MCP server/OAuth proxy, it doesn't need access to any device APIs.

Implementation

Permissions-Policy Middleware

File: src/middleware/permissions-policy.ts

Created a custom Express middleware that sets the Permissions-Policy header:

typescript
export function permissionsPolicyMiddleware(
  _req: Request,
  res: Response,
  next: NextFunction,
): void {
  const policies = [
    "camera=()",                  // No camera access
    "microphone=()",              // No microphone access
    "geolocation=()",             // No location access
    "payment=()",                 // No payment APIs
    "usb=()",                     // No USB device access
    "magnetometer=()",            // No magnetometer access
    "gyroscope=()",               // No gyroscope access
    "accelerometer=()",           // No accelerometer access
    "ambient-light-sensor=()",    // No ambient light sensor
    "autoplay=()",                // No autoplay
    "encrypted-media=()",         // No encrypted media (DRM)
    "fullscreen=()",              // No fullscreen API
    "midi=()",                    // No MIDI device access
    "picture-in-picture=()",      // No picture-in-picture
    "speaker-selection=()",       // No speaker selection
    "sync-xhr=()",                // No synchronous XHR
    "vr=()",                      // No VR APIs (deprecated)
    "xr-spatial-tracking=()",     // No XR spatial tracking
    "display-capture=()",         // No screen capture
  ];

  res.setHeader("Permissions-Policy", policies.join(", "));
  next();
}

Why Custom Middleware?

  • Helmet 8.x doesn't support Permissions-Policy configuration in the options object
  • Custom middleware provides explicit control over all policies
  • Easier to document and maintain than relying on helmet internals
  • Can be easily modified if requirements change

Middleware Application

File: src/app.ts:20

Applied middleware globally after helmet:

typescript
// Security headers - Apply BEFORE other middleware
app.use(helmet(config.helmet));
app.use(permissionsPolicyMiddleware); // Add Permissions-Policy header

Placement Rationale:

  • Applied after helmet to ensure all security headers are set together
  • Applied before other middleware to ensure header is set on all responses
  • Global application ensures consistent security posture across all routes

Test Coverage

File: src/config/helmet.test.ts:74-125

Added comprehensive tests for the Permissions-Policy header:

1. Basic Header Presence Test:

typescript
it("should set Permissions-Policy header", async () => {
  const response = await request(app).get("/health");

  const policy = response.headers["permissions-policy"];
  expect(policy).toBeDefined();
  expect(typeof policy).toBe("string");

  if (typeof policy === "string") {
    // Verify critical features are disabled
    expect(policy).toContain("camera=()");
    expect(policy).toContain("microphone=()");
    expect(policy).toContain("geolocation=()");
    expect(policy).toContain("payment=()");
    expect(policy).toContain("usb=()");
  }
});

2. Comprehensive Feature Disable Test:

typescript
it("should disable all sensitive features in Permissions-Policy", async () => {
  const response = await request(app).get("/health");

  const policy = response.headers["permissions-policy"];
  expect(policy).toBeDefined();
  expect(typeof policy).toBe("string");

  if (typeof policy === "string") {
    const disabledFeatures = [
      "camera", "microphone", "geolocation", "payment", "usb",
      "magnetometer", "gyroscope", "accelerometer",
      "ambient-light-sensor", "autoplay", "encrypted-media",
      "fullscreen", "midi", "picture-in-picture",
      "speaker-selection", "sync-xhr", "display-capture",
    ];

    // Verify each feature is disabled (set to empty allowlist)
    for (const feature of disabledFeatures) {
      expect(policy).toContain(`${feature}=()`);
    }
  }
});

Benefits

Security

  1. Defense in Depth: Additional security layer beyond authentication and CORS
  2. Attack Surface Reduction: Explicitly disables unused browser features
  3. Side-Channel Protection: Prevents sensor-based attacks (gyroscope, accelerometer, magnetometer)
  4. Device Access Control: Blocks unauthorized access to camera, microphone, USB
  5. Location Privacy: Prevents geolocation API access

Compliance

  1. Security Best Practices: Aligns with OWASP recommendations
  2. Browser Security Standards: Implements W3C Permissions Policy specification
  3. Security Audits: Demonstrates proactive security posture

Operational

  1. No Performance Impact: Header is set once per response
  2. No Breaking Changes: Seed doesn't use any of the disabled features
  3. Easy to Maintain: Single middleware file with clear documentation
  4. Future-Proof: Can easily add or remove policies as needed

Permissions-Policy Format

The Permissions-Policy header uses a structured syntax:

Permissions-Policy: feature1=(allowlist), feature2=(allowlist), ...

Allowlist Options:

  • () - Empty allowlist (feature disabled for all origins) - Used in Seed
  • 'self' - Feature allowed for same origin only
  • 'src' - Feature allowed for iframe src URL
  • * - Feature allowed for all origins (not recommended)
  • https://example.com - Feature allowed for specific origin

Example Response Header:

http
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()

Features Disabled

Hardware Access

  • camera: Webcam access
  • microphone: Audio input
  • usb: USB device enumeration and access
  • midi: MIDI device access
  • speaker-selection: Speaker/audio output selection

Sensors

  • geolocation: GPS and location services
  • magnetometer: Compass and magnetic field sensor
  • gyroscope: Orientation sensor
  • accelerometer: Motion sensor
  • ambient-light-sensor: Light level sensor

Media & Display

  • autoplay: Automatic media playback
  • encrypted-media: DRM-protected media (EME)
  • fullscreen: Fullscreen API
  • picture-in-picture: PiP mode
  • display-capture: Screen capture/sharing

APIs

  • payment: Payment Request API
  • sync-xhr: Synchronous XMLHttpRequest
  • vr: WebVR API (deprecated, included for older browsers)
  • xr-spatial-tracking: WebXR spatial tracking

Attack Scenarios Mitigated

1. Side-Channel Attacks

Attack: Malicious script uses gyroscope/accelerometer to infer keystrokes or UI interactions Mitigation: gyroscope=(), accelerometer=() prevents sensor access

2. Location Tracking

Attack: Script attempts to track user location via geolocation API Mitigation: geolocation=() blocks location access

3. Device Fingerprinting

Attack: Script enumerates USB/MIDI devices for fingerprinting Mitigation: usb=(), midi=() prevents device enumeration

4. Screen Recording

Attack: Malicious iframe attempts screen capture Mitigation: display-capture=() blocks screen sharing APIs

5. Payment API Abuse

Attack: Phishing attempt via Payment Request API Mitigation: payment=() disables payment APIs

Browser Support

Well-Supported (90%+ global coverage):

  • Chrome/Edge 88+
  • Firefox 74+
  • Safari 15.4+
  • Opera 74+

Partial Support:

  • Older browsers ignore unknown features (graceful degradation)
  • Feature names may vary between browsers (policy includes variations)

Fallback Behavior:

  • Browsers that don't support Permissions-Policy simply ignore the header
  • No breaking changes for older browsers
  • Feature blocking is a progressive enhancement

Configuration

No configuration required. The middleware applies a fixed policy suitable for an MCP server/OAuth proxy.

To modify policies (if needed):

  1. Edit src/middleware/permissions-policy.ts
  2. Add/remove features from the policies array
  3. Update tests in src/config/helmet.test.ts

Example modification (allow fullscreen):

typescript
const policies = [
  // ... other policies
  "fullscreen='self'",  // Allow fullscreen from same origin
  // ... rest of policies
];

Limitations

  1. Browser-Only Protection: Only effective in browser environments
  2. Not a Primary Control: Complements but doesn't replace authentication/authorization
  3. Graceful Degradation: Older browsers ignore the header (acceptable trade-off)
  4. Static Policy: Not dynamically configurable (appropriate for Seed's use case)

Future Enhancements

  1. Dynamic Policies: Allow per-route policy customization if needed
  2. Policy Reporting: Add Permissions-Policy-Report-Only for monitoring violations
  3. Additional Features: Monitor specification for new browser features to restrict
  4. Configuration Option: Add environment variable to customize policies (if requirements change)
  • Gap Analysis: Section 9.1 "Missing Permissions-Policy Header"
  • Security Architecture: security.md
  • Related Enhancements:
    • Helmet Security Headers - CSP, HSTS, X-Frame-Options
    • CORS Policy - Origin validation
    • Rate Limiting - Request throttling

Implementation Date

Completed: 2026-01-06 Actual Effort: 30 minutes Test Coverage: 2 tests (basic header presence + comprehensive feature checking) Validation: All 737 tests passing

References

Released under the MIT License.