# Permissions-Policy reporting (Apache) The live `Permissions-Policy` header denies features with an empty allowlist (`feature=()` — grant to nobody). That header does **not** send reports by itself. Unlike CSP, Permissions-Policy has **no `report-uri`**. Reporting uses the Reporting API: a named endpoint in `Reporting-Endpoints`, plus a per-feature `report-to` parameter. Reuse the existing CSP collector: - UAT: `https://pnspsuat.gld.gov.hk/api/csp-report` - PROD: `https://pnsps.gld.gov.hk/api/csp-report` Backend: `CspReportController` (`POST /csp-report`) already logs the raw JSON and `Content-Type` and returns `204`. No backend change is required. See also: [csp-apache.conf.md](./csp-apache.conf.md) ## Why not `report-uri` This CSP-style trailer does **not** produce Permissions-Policy reports: ```apache # Wrong — browsers ignore report-uri on Permissions-Policy Header always set Permissions-Policy "camera=(), geolocation=(); report-uri https://pnspsuat.gld.gov.hk/api/csp-report" ``` `report-to` is a **parameter of each feature**, not a global trailing directive. ## Does every feature need `report-to`? Yes, **if you want a report for that feature**. - `feature=()` — still blocked, but silent (no report) - `feature=();report-to=csp-endpoint` — blocked **and** reported There is no `all=()` reporter and no header-level `report-to` that applies to every feature. Add `;report-to=csp-endpoint` only on features you want in the log. Leave the rest as `()` if you do not need those reports. ```apache # camera is reported; geolocation is still blocked, but not reported Header always set Permissions-Policy "camera=();report-to=csp-endpoint, geolocation=()" ``` ## Enforcing + reporting Keep the current deny-all policy. Add `Reporting-Endpoints`, then append `;report-to=csp-endpoint` on each feature you want to monitor. Copy the feature list from the live header; only the `report-to` parameter is new. Example (UAT): ```apache Header always set Reporting-Endpoints "csp-endpoint=\"https://pnspsuat.gld.gov.hk/api/csp-report\"" Header always set Permissions-Policy "\ accelerometer=();report-to=csp-endpoint, \ autoplay=();report-to=csp-endpoint, \ camera=();report-to=csp-endpoint, \ display-capture=();report-to=csp-endpoint, \ encrypted-media=();report-to=csp-endpoint, \ fullscreen=();report-to=csp-endpoint, \ geolocation=();report-to=csp-endpoint, \ gyroscope=();report-to=csp-endpoint, \ magnetometer=();report-to=csp-endpoint, \ microphone=();report-to=csp-endpoint, \ midi=();report-to=csp-endpoint, \ payment=();report-to=csp-endpoint, \ picture-in-picture=();report-to=csp-endpoint, \ publickey-credentials-get=();report-to=csp-endpoint, \ screen-wake-lock=();report-to=csp-endpoint, \ usb=();report-to=csp-endpoint, \ web-share=();report-to=csp-endpoint, \ xr-spatial-tracking=();report-to=csp-endpoint" ``` PROD: same headers, with `https://pnsps.gld.gov.hk/api/csp-report` Because deny-all is already enforced, put `report-to` on **`Permissions-Policy`** (not only Report-Only). Those reports have `disposition: "enforce"`. ## Report-Only (observe without blocking) Use `Permissions-Policy-Report-Only` only when testing a restriction that is **not** already denied by the enforcing header. Reports have `disposition: "report"`. ```apache Header always set Reporting-Endpoints "csp-endpoint=\"https://pnspsuat.gld.gov.hk/api/csp-report\"" Header always set Permissions-Policy-Report-Only "geolocation=();report-to=csp-endpoint" ``` Report-Only cannot re-enable a feature already denied by `Permissions-Policy`. ## Same URL, different payload Keep CSP on `report-uri` as it is. The collector URL is shared; the body is not. | Source | `Content-Type` | Body | | --- | --- | --- | | CSP `report-uri` | `application/csp-report` | `{ "csp-report": { … } }` | | Permissions-Policy | `application/reports+json` | JSON **array**, `type` = `permissions-policy-violation` | Example Permissions-Policy report: ```json [{ "type": "permissions-policy-violation", "url": "https://pnspsuat.gld.gov.hk/", "body": { "disposition": "enforce", "featureId": "geolocation", "message": "Permissions policy violation: geolocation access has been blocked because of a permissions policy applied to the current document." } }] ``` Chrome often serializes the feature as `policyId` instead of `featureId`. Filter logs on `permissions-policy-violation` vs `csp-report` so the two streams stay distinct. Optional: also point CSP at the same named endpoint (CSP `report-uri` remains for older browsers): ```apache Header always set Content-Security-Policy-Report-Only "…; report-uri https://pnspsuat.gld.gov.hk/api/csp-report; report-to csp-endpoint" ``` ## Follow-up checklist - [ ] Confirm the live Apache `Permissions-Policy` feature list (deny-all `()`). - [ ] Add `Reporting-Endpoints` → existing `/api/csp-report` (UAT vs PROD host). - [ ] Add `;report-to=csp-endpoint` only on features you want in the log. - [ ] Deploy to UAT first; trigger a blocked API (e.g. `navigator.geolocation`) in Chrome/Edge. - [ ] Confirm a `permissions-policy-violation` line in the backend log (may be batched, a few seconds later). - [ ] Repeat on PROD with the PROD report URL. ## Notes - Reporting is Chromium-only (Chrome / Edge). Safari and Firefox still enforce `()` and usually send nothing. - Reports are batched and may arrive a few seconds after the violation, not on the same page request. - Same-origin `/api/csp-report` needs no extra CORS setup. The endpoint is already unauthenticated and CSRF is disabled. - Do not allowlist a feature in `Permissions-Policy` just to silence a report. Only grant a feature if the application itself needs it.