|
|
@@ -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. |