diff --git a/docs/csp-apache.conf.md b/docs/csp-apache.conf.md index c7d578e7..cdce4c4a 100644 --- a/docs/csp-apache.conf.md +++ b/docs/csp-apache.conf.md @@ -6,6 +6,7 @@ Adjust `report-uri` if your API base path or host differs (UAT example below; PR See also: +- [permissions-policy-apache.conf.md](./permissions-policy-apache.conf.md) — Permissions-Policy reporting via the same `/api/csp-report` URL - [csp-report-review-2026-08-14.md](./csp-report-review-2026-08-14.md) — latest PROD + UAT report review and add/remove summary - [csp-report-review-2026-08-03.md](./csp-report-review-2026-08-03.md) — earlier PROD report analysis that drove `frame-src` / `media-src` updates diff --git a/docs/permissions-policy-apache.conf.md b/docs/permissions-policy-apache.conf.md new file mode 100644 index 00000000..9fdb4a65 --- /dev/null +++ b/docs/permissions-policy-apache.conf.md @@ -0,0 +1,152 @@ +# 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.