Jason Chuang 1 день назад
Родитель
Сommit
27e5282614
2 измененных файлов: 153 добавлений и 0 удалений
  1. +1
    -0
      docs/csp-apache.conf.md
  2. +152
    -0
      docs/permissions-policy-apache.conf.md

+ 1
- 0
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



+ 152
- 0
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.

Загрузка…
Отмена
Сохранить