Procházet zdrojové kódy

Merge remote-tracking branch 'origin/master' into CR026

CR026
Jason Chuang před 16 hodinami
rodič
revize
e84d0b52c4
11 změnil soubory, kde provedl 363 přidání a 10 odebrání
  1. +3
    -1
      docs/csp-apache.conf.md
  2. +1
    -0
      docs/csp-report-review-2026-08-03.md
  3. +1
    -0
      docs/csp-report-review-2026-08-14.md
  4. +197
    -0
      docs/csp-report-review-2026-09-22.md
  5. +152
    -0
      docs/permissions-policy-apache.conf.md
  6. binární
      src/assets/images/wcag2AA.png
  7. +1
    -1
      src/components/cards/AuthFooter.js
  8. +5
    -5
      src/pages/Payment/MultiPaymentWindow.js
  9. +1
    -1
      src/translations/en.json
  10. +1
    -1
      src/translations/zh-CN.json
  11. +1
    -1
      src/translations/zh-HK.json

+ 3
- 1
docs/csp-apache.conf.md Zobrazit soubor

@@ -6,7 +6,9 @@ Adjust `report-uri` if your API base path or host differs (UAT example below; PR

See also:

- [csp-report-review-2026-08-14.md](./csp-report-review-2026-08-14.md) — latest PROD + UAT report review and add/remove summary
- [permissions-policy-apache.conf.md](./permissions-policy-apache.conf.md) — Permissions-Policy reporting via the same `/api/csp-report` URL
- [csp-report-review-2026-09-22.md](./csp-report-review-2026-09-22.md) — latest PROD report review (`All_CSP.txt`)
- [csp-report-review-2026-08-14.md](./csp-report-review-2026-08-14.md) — earlier 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

## Enforcing


+ 1
- 0
docs/csp-report-review-2026-08-03.md Zobrazit soubor

@@ -13,6 +13,7 @@ Related app docs:

- [csp-apache.conf.md](./csp-apache.conf.md) — Apache header snippets to deploy
- [csp-report-review-2026-08-14.md](./csp-report-review-2026-08-14.md) — later PROD + UAT review (add/remove summary)
- [csp-report-review-2026-09-22.md](./csp-report-review-2026-09-22.md) — later PROD review (`All_CSP.txt`)

---



+ 1
- 0
docs/csp-report-review-2026-08-14.md Zobrazit soubor

@@ -14,6 +14,7 @@ Related app docs:

- [csp-apache.conf.md](./csp-apache.conf.md) — Apache header snippets to deploy
- [csp-report-review-2026-08-03.md](./csp-report-review-2026-08-03.md) — earlier PROD review
- [csp-report-review-2026-09-22.md](./csp-report-review-2026-09-22.md) — later PROD review (`All_CSP.txt`)

---



+ 197
- 0
docs/csp-report-review-2026-09-22.md Zobrazit soubor

@@ -0,0 +1,197 @@
# CSP Report Review (PROD, 2026-09-22)

Review of Content-Security-Policy-Report-Only violations from PNSPS PROD
(`https://pnsps.gld.gov.hk`).

Source logs (external):

- `All_CSP.txt` — 196 reports, 2026-08-27 to 2026-09-17

Related app docs:

- [csp-apache.conf.md](./csp-apache.conf.md) — Apache header snippets to deploy
- [csp-report-review-2026-08-14.md](./csp-report-review-2026-08-14.md) — earlier PROD + UAT review
- [csp-report-review-2026-08-03.md](./csp-report-review-2026-08-03.md) — earlier PROD review

---

## Verdict

**No app-side CSP concern in this dump.** Almost every line is a browser
extension, not PNSPS code. `/proof/reply/{id}` is just a busy authenticated
page, so the same extension noise shows up there a lot.

CSP is still **Report-Only** (`disposition: report`) — the browser logged these
hits and **did not block the page**.

The 2026-08-14 app-owned issue (proof PDF preview `frame-src`) is **gone** from
this dump. Live PROD now includes `frame-src 'self' data: blob:;`.

---

## Add / remove summary

**PROD — add nothing required. Remove nothing.**

Keep `script-src 'self'`. Do not add `'unsafe-inline'`, `'unsafe-eval'`, or
`'wasm-unsafe-eval'`.

**Optional (low priority, already in [csp-apache.conf.md](./csp-apache.conf.md)):**

```apache
media-src 'self' blob: data:;
```

Live PROD is still `media-src 'self' blob:` (no `data:`). This dump has only
2 matching hits in three weeks.

**Do not add:** `'wasm-unsafe-eval'`, Perplexity CDN, Youdao, NetEase CDN,
Google Fonts.

---

## Confirmed current PROD policy (Report-Only)

This is the live header that generated every report in `All_CSP.txt`. It matches
`original-policy` in all 196 violations.

```apache
Header always set Content-Security-Policy-Report-Only "default-src 'self'; \
base-uri 'self'; \
object-src 'none'; \
frame-ancestors 'none'; \
frame-src 'self' data: blob:; \
form-action 'self'; \
script-src 'self'; \
style-src 'self' 'unsafe-inline'; \
style-src-elem 'self' 'unsafe-inline'; \
img-src 'self' data:; \
media-src 'self' blob:; \
font-src 'self' data:; \
connect-src 'self'; \
upgrade-insecure-requests; \
report-uri https://pnsps.gld.gov.hk/api/csp-report"
```

Changes vs the 2026-08-14 live header:

- `frame-src 'self' data: blob:;` is now present.
- `img-src` no longer allowlists `https://www.w3.org` / `https://w3.org`
(badge is hosted locally).

`style-src 'unsafe-inline'` is expected for MUI and is the only real policy
weakness. These reports do not show a new XSS issue.

---

## Summary of findings

| Count | Directive | Blocked | Verdict |
|---:|---|---|---|
| **192** | `script-src` | `wasm-eval` | Noise — Chrome extension |
| 2 | `media-src` | `data` | Optional leftover — `data:` not in live `media-src` |
| 1 | `font-src` | `frontend-cdn.perplexity.ai` … `FKGroteskNeue.woff2` | Noise — Perplexity sidebar |
| 1 | `img-src` | `ydlunacommon-cdn.nosdn.127.net` … `.svg` | Noise — Youdao / NetEase plugin |

No `frame-src` reports. No app `eval`. No Google Fonts.

---

## Why `wasm-eval` volume is high

192 of 196 reports (98%) are `script-src` / `wasm-eval`.

| Count | `source-file` | Notes |
|---:|---|---|
| 103 | `chrome-extension` | Every one with a location is **line 23, column 39316** — the same injected script |
| 89 | (none) | Chrome often sends a second report for the same violation without `source-file` |

About 46 timestamps have 2+ reports in the same second (paired duplicates).
That is why the log looks twice as large as the number of real events.

Password managers, translators, Grammarly-style tools, and similar extensions
compile WebAssembly into the page. `script-src 'self'` (without
`'wasm-unsafe-eval'`) reports that. Do **not** loosen `script-src` to silence
it.

If quieter logs are needed, filter `blocked-uri=wasm-eval` plus
`source-file=chrome-extension` (and the paired empty-source twin).

---

## Why many `/proof/reply` reports

| Count | Page group |
|---:|---|
| 90 | `/login` |
| **38** | `/proof/reply/{id}` |
| 23 | `/publicNotice/{id}` |
| 15 | `/publicNotice/apply` |
| 10 | `/publicNotice` |
| 10 | `/proof/search` |
| 4 | `/paymentPage/*` |
| 3 | `/user/changePassword` |
| 2 | `/registerFromOrganization` |
| 1 | `/verify/*` |

The 38 Reply Proof hits span **12 proof IDs** (`29518`, `29948`, `30112`,
`30114`, `30134`, `30166`, `30190`, `30266`, `30306`, `30326`, `30484`,
`30958`). They are the same `wasm-eval` + Chrome extension pattern as login,
apply, and notice detail.

Reply Proof is a long, authenticated screen. Anyone with a WASM extension
triggers a report on every visit. **No Reply Proof code change is required.**

---

## Not a PNSPS SPA problem — do not allowlist

| Count | Page | Directive | Blocked | Why ignore |
|---:|---|---|---|---|
| 192 | many (login 90, reply 38, …) | `script-src` | `wasm-eval` | `chrome-extension` (line 23, col 39316) or paired empty source |
| 1 | `/login` | `font-src` | Perplexity `FKGroteskNeue.woff2` | Sidebar / extension |
| 1 | `/login` | `img-src` | NetEase `ydlunacommon-cdn.nosdn.127.net` | Youdao-style translator |
| 2 | `/publicNotice/apply`, `/proof/reply/30166` | `media-src` | `data` | Optional `data:` gap only; 2 hits in three weeks |

`status-code: 404` on 166 reports is typical SPA client-route noise, not a
broken document.

---

## Optional: `media-src` and `data:`

Live policy: `media-src 'self' blob:;`

Captcha audio already uses `blob:` (`CustomFormWizard.js` and similar). Adding
`data:` is low risk and already documented:

```apache
media-src 'self' blob: data:;
```

Not required for this dump.

---

## Recommended actions

1. **No Apache change required** for these reports. `frame-src` deploy from
2026-08-14 looks effective (zero `frame-src` hits here).
2. Keep PROD at `script-src 'self'`. Do not add `'unsafe-eval'` or
`'wasm-unsafe-eval'`.
3. Optional: add `data:` to `media-src` to match
[csp-apache.conf.md](./csp-apache.conf.md).
4. Keep Report-Only until remaining volume is understood as extension noise;
then promote the same policy to enforcing.
5. Optional log hygiene: drop or sample `wasm-eval` + `chrome-extension` so
real app regressions stay visible.

No application code change is required.

---

## Backend reference

- Report endpoint: `POST /csp-report` (context path → `/api/csp-report`)
- Controller: `CspReportController` in PNSPS-backend
- Frontend/Apache CSP above is what browsers apply to the SPA document

+ 152
- 0
docs/permissions-policy-apache.conf.md Zobrazit soubor

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

binární
src/assets/images/wcag2AA.png Zobrazit soubor

Před Za
Šířka: 368  |  Výška: 130  |  Velikost: 11 KiB Šířka: 368  |  Výška: 130  |  Velikost: 7.4 KiB

+ 1
- 1
src/components/cards/AuthFooter.js Zobrazit soubor

@@ -58,7 +58,7 @@ const AuthFooter = () => {
{!isGLDLoggedIn()? (
<a
href="https://www.w3.org/WAI/WCAG2AA-Conformance"
title="Explanation of WCAG 2 Level AA conformance"
title={wcagAlt}
>
<img
height="32"


+ 5
- 5
src/pages/Payment/MultiPaymentWindow.js Zobrazit soubor

@@ -408,11 +408,11 @@ const MultiPaymentWindow = (props) => {
</Grid>
: null}
</Grid>:
<Grid container direction="row" justifyContent="center" alignItems="center">
<FormLabel sx={{ fontSize: "20px", color: "#000000", textAlign: "center"}}>
<FormattedMessage id="paymentMethodNotAvailable"/>
</FormLabel>
</Grid>
<Grid container direction="row" justifyContent="flex-start" alignItems="center">
<FormLabel sx={{ fontSize: "20px", color: "#000000", textAlign: "left" }}>
<FormattedMessage id="paymentMethodNotAvailable"/>
</FormLabel>
</Grid>
}
</Grid>
<Grid item xs={12} md={12}>


+ 1
- 1
src/translations/en.json Zobrazit soubor

@@ -14,7 +14,7 @@

"bhkLogoAlt": "Brand Hong Kong logo",
"iAmSmartAlt": "iAM Smart",
"wcagAaAlt": "Level AA conformance, W3C WAI Web Content Accessibility Guidelines 2.0",
"wcagAaAlt": "Level AA conformance, W3C WAI Web Content Accessibility Guidelines 2.2",
"downloadPdfAria": "Download PDF: {guide} ({userType})",
"captchaPlayAudio": "Play audio CAPTCHA",



+ 1
- 1
src/translations/zh-CN.json Zobrazit soubor

@@ -14,7 +14,7 @@

"bhkLogoAlt": "香港品牌标志",
"iAmSmartAlt": "智方便",
"wcagAaAlt": "符合 WCAG 2.0 AA 级别(W3C WAI 网页内容无障碍指引)",
"wcagAaAlt": "符合 WCAG 2.2 AA 级别(W3C WAI 网页内容无障碍指引)",
"downloadPdfAria": "下载 PDF:{guide}({userType})",
"captchaPlayAudio": "播放语音验证码",


+ 1
- 1
src/translations/zh-HK.json Zobrazit soubor

@@ -14,7 +14,7 @@

"bhkLogoAlt": "香港品牌標誌",
"iAmSmartAlt": "智方便",
"wcagAaAlt": "符合 WCAG 2.0 AA 級別(W3C WAI 網頁內容無障礙指引)",
"wcagAaAlt": "符合 WCAG 2.2 AA 級別(W3C WAI 網頁內容無障礙指引)",
"downloadPdfAria": "下載 PDF:{guide}({userType})",
"captchaPlayAudio": "播放語音驗證碼",



Načítá se…
Zrušit
Uložit