From 606dbcf40e09300e37d6ab28ff61802e62fd1fdb Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Sat, 8 Aug 2026 02:53:32 +0800 Subject: [PATCH 01/22] remove ldap --- src/auth/index.js | 2 +- src/auth/jwt/jwtService.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/auth/index.js b/src/auth/index.js index 57760507..06f94af4 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -22,7 +22,7 @@ let expiredAlertShownInMemory = false; /** Login / public auth endpoints must not trigger token refresh or session-expiry reload. */ const isPublicAuthRequest = (reqUrl) => - reqUrl.includes('/login') || reqUrl.includes('/ldap-login'); + reqUrl.includes('/login'); /** Clear stale session-expiry flag so a new login attempt can show its own error dialog. */ export const clearExpiredSessionAlert = () => { diff --git a/src/auth/jwt/jwtService.js b/src/auth/jwt/jwtService.js index 884d090c..430717a0 100644 --- a/src/auth/jwt/jwtService.js +++ b/src/auth/jwt/jwtService.js @@ -46,7 +46,6 @@ export default class JwtService { if (response && response.status === 401) { const isPublicAuthRequest = reqUrl.includes('/login') || - reqUrl.includes('/ldap-login') || reqUrl.includes('/refresh-token') if (isPublicAuthRequest) { return Promise.reject(error) From 10a6153ee3fc89ffb85018b3dd4ae2cbceabc73e Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Sat, 15 Aug 2026 10:17:14 +0800 Subject: [PATCH 02/22] updated CR025, label change --- src/pages/Payment/Card/index.js | 4 +- src/pages/Payment/FPS/FPS.js | 74 ++++++++++++------- src/pages/Payment/FPS/FPSTest.js | 4 +- src/pages/Payment/MultiPaymentWindow.js | 26 +++---- .../auth-forms/PasswordAlertDialog.js | 3 +- src/pages/dashboard/Public/index.js | 3 +- src/themes/colorConst.js | 11 +++ src/translations/en.json | 11 ++- src/translations/zh-CN.json | 11 ++- src/translations/zh-HK.json | 11 ++- 10 files changed, 101 insertions(+), 57 deletions(-) diff --git a/src/pages/Payment/Card/index.js b/src/pages/Payment/Card/index.js index 7294907a..c7ecff5c 100644 --- a/src/pages/Payment/Card/index.js +++ b/src/pages/Payment/Card/index.js @@ -24,6 +24,7 @@ const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/Loa import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import {FormattedMessage} from "react-intl"; +import { ERROR_CONTAINED_BUTTON_SX } from "themes/colorConst"; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, width: '100%', @@ -161,11 +162,10 @@ const Index = () => { component="span" variant="contained" size="large" - color="error" onClick={() => { cancelPayment(); }} - sx={{ m: 4 }} + sx={{ m: 4, ...ERROR_CONTAINED_BUTTON_SX }} > diff --git a/src/pages/Payment/FPS/FPS.js b/src/pages/Payment/FPS/FPS.js index 5b9f7d97..d15031b2 100644 --- a/src/pages/Payment/FPS/FPS.js +++ b/src/pages/Payment/FPS/FPS.js @@ -4,6 +4,7 @@ import { Typography, Stack, Button, + Box, } from '@mui/material'; import * as React from "react"; import * as HttpUtils from "utils/HttpUtils"; @@ -23,6 +24,7 @@ const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/Loa import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import {FormattedMessage, useIntl} from "react-intl"; +import { PRIMARY_CONTAINED_BUTTON_SX, ERROR_CONTAINED_BUTTON_SX } from "themes/colorConst"; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, width: '100%', @@ -399,9 +401,23 @@ const Index = () => {


- - -

+ + {!isError && + + + + } + {isError && + + + +
  • +
  • +
    +
    + } +
    + FPS
    @@ -409,12 +425,8 @@ const Index = () => { {"HK$ " + currencyFormat(paymentData.amount)}


    - {isError ? - - - - : - browserType==mobileBrowser? + {!isError && + (browserType==mobileBrowser? { sysEnv=="prod"? @@ -480,26 +492,32 @@ const Index = () => { } - } + )} - + {isError ? + + : + + }
    diff --git a/src/pages/Payment/FPS/FPSTest.js b/src/pages/Payment/FPS/FPSTest.js index 83da857d..cd34e35b 100644 --- a/src/pages/Payment/FPS/FPSTest.js +++ b/src/pages/Payment/FPS/FPSTest.js @@ -18,6 +18,7 @@ const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/Loa import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import {FormattedMessage} from "react-intl"; +import { ERROR_CONTAINED_BUTTON_SX } from "themes/colorConst"; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, width: '100%', @@ -322,11 +323,10 @@ const Index = () => { component="span" variant="contained" size="large" - color="error" onClick={()=>{ cancelPayment(); }} - sx={{ m: 4 }} + sx={{ m: 4, ...ERROR_CONTAINED_BUTTON_SX }} > diff --git a/src/pages/Payment/MultiPaymentWindow.js b/src/pages/Payment/MultiPaymentWindow.js index 78cd292f..cf0315b0 100644 --- a/src/pages/Payment/MultiPaymentWindow.js +++ b/src/pages/Payment/MultiPaymentWindow.js @@ -277,23 +277,21 @@ const MultiPaymentWindow = (props) => { maxWidth={'xl'} fullScreen={props.isFullScreen} > - - - - - - {windowTitle} - - - - + + + {windowTitle} +
    - - - - + + + + +
  • +
  • +
  • +
    diff --git a/src/pages/authentication/auth-forms/PasswordAlertDialog.js b/src/pages/authentication/auth-forms/PasswordAlertDialog.js index f6a38351..846ba1d0 100644 --- a/src/pages/authentication/auth-forms/PasswordAlertDialog.js +++ b/src/pages/authentication/auth-forms/PasswordAlertDialog.js @@ -17,6 +17,7 @@ import { } from '@mui/material'; import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; import {FormattedMessage} from "react-intl"; +import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; const PasswordAlertDialog = (props) => { return ( @@ -63,7 +64,7 @@ const PasswordAlertDialog = (props) => {
    - diff --git a/src/pages/dashboard/Public/index.js b/src/pages/dashboard/Public/index.js index d5f99775..fad79e47 100644 --- a/src/pages/dashboard/Public/index.js +++ b/src/pages/dashboard/Public/index.js @@ -12,6 +12,7 @@ import { import { isORGLoggedIn, } from "utils/Utils"; import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import { FormattedMessage, useIntl } from "react-intl"; +import { ERROR_CONTAINED_BUTTON_SX } from "themes/colorConst"; import AdsClickRoundedIcon from '@mui/icons-material/AdsClickRounded'; import * as React from "react"; import Loadable from 'components/Loadable'; @@ -300,7 +301,7 @@ const DashboardDefault = () => {
    - + diff --git a/src/themes/colorConst.js b/src/themes/colorConst.js index cecd4c9e..32074a43 100644 --- a/src/themes/colorConst.js +++ b/src/themes/colorConst.js @@ -58,6 +58,17 @@ export const CONTAINED_PRIMARY_BLUE = '#0c489e'; /** Validation error text/border — matches FormHelperText in styles.css (e.g. "Please enter password"). WCAG AA on white. */ export const VALIDATION_ERROR_COLOR = '#B00020'; +/** WCAG 2.0 AA contained error button red — lightest tone passing 4.5:1 with white text (~4.57:1). */ +export const CONTAINED_ERROR_RED = '#e61f2a'; + +export const ERROR_CONTAINED_BUTTON_SX = { + backgroundColor: CONTAINED_ERROR_RED, + color: '#FFFFFF', + '&:hover': { + backgroundColor: '#d32f2f', + }, +}; + export const PRIMARY_CONTAINED_BUTTON_SX = { backgroundColor: CONTAINED_PRIMARY_BLUE, color: '#FFFFFF', diff --git a/src/translations/en.json b/src/translations/en.json index 80ad1c4f..8016770c 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -121,6 +121,7 @@ "iAmSmartNoIdNoMsg": "Invalid information, please return to the creation of account page.", "mainPage": "Main Page", + "backToMainPage": "Back to Main Page", "myPublicNotice": "My Public Notices", "publicNotice": "Public Notice", "publicNoticeApp": "Public Notice Application", @@ -418,7 +419,7 @@ "payIdNRefer": "Payment No. / Payment Reference No.", "payConfirm": "Confirm payment", "payCancel": "Cancel payment", - "payAlert": "Please don’t close this window, you may either complete this payment or cancel this payment by the button at the bottom of this page.", + "payAlert": "Please do not refresh or close this page during payment to avoid payment failure. In case of cancellation, please use the “Cancel Payment” button at the bottom of this page.", "payTotalDeatail": "Total Payment Amount", "payDeatail": "Total Payment Amount", "payTotal": "Total Payment Amount", @@ -451,7 +452,9 @@ "paymentLimitPrice2":" is only applicable when minimum amount is HK$0.10 and maximum amount is HK$9,999,999.99", "paymentLimitPPS":" Payment could not be made via mobile device browsers, please use desktop computers to make payment.", "paymentMethod": "Payment Method", - "paymentProcessLimited":"Please complete the payment process within 15 minutes. Note: For FPS payments, scanning, payment and all necessary approvals must be finished within 3 minutes due to security-related QR code expiry.", + "paymentProcessLimited1":"Please complete the payment process within 15 minutes.", + "paymentProcessLimited2":"Please do not refresh or close any page during payment to avoid payment failure. In case of cancellation, please use the “Cancel Payment” button at the bottom of this page.", + "paymentProcessLimited3":"Note: For FPS payments, scanning, payment and all necessary approvals must be finished within 3 minutes due to security-related QR code expiry.", "publicNoticeDetailTitle": "Public Notice Application Information", "applyPerson": "Applicant", @@ -515,7 +518,9 @@ "fpsQrcodeTitle4":"Remaining time:", "fpsQrcodeTitle5":"s", "fpsQrcodeExpired":"QR code has expired.", - "fpsPaymentErrorMsg":"An error occurred while loading the payment QR code. Please do not refresh/reload this page manually during the payment. If the payment was not made successfully, please click 'Cancel payment' button and make the payment again. Sorry for the inconvenience caused.", + "fpsPaymentErrorMsg1":"The payment QR code is no longer valid after reloading the page. Please verify your banking transaction history:", + "fpsPaymentErrorMsg2":"If payment was deducted: Please contact Accounts Section (Tel.: 2231 5183/2231 5318) with your transaction details.", + "fpsPaymentErrorMsg3":"If payment was not deducted: Please click \"Back to Main Page\" and try again after 30 minutes.", "fpsSelectPaymentApp":"Please Select Bank App", "payDnRemark": "Payment proof (e.g. ATM receipt, internet banking record) to be sent to gld_acct@gld.gov.hk by {date} 12:30 p.m.", diff --git a/src/translations/zh-CN.json b/src/translations/zh-CN.json index 2b6f68e3..d3b90e5a 100644 --- a/src/translations/zh-CN.json +++ b/src/translations/zh-CN.json @@ -90,7 +90,9 @@ "fpsQrcodeTitle4":"剩余时间:", "fpsQrcodeTitle5":"秒", "fpsQrcodeExpired":"二维码已过期", - "fpsPaymentErrorMsg":"载入支付二维码时发生错误。请勿在付款过程中更新此页面。如果付款未完成,请点击「取消支付」按钮并重新支付。由此造成的不便,敬请谅解。", + "fpsPaymentErrorMsg1":"页面重新加载时发生错误,支付二维码已失效。请先确认银行交易纪录:", + "fpsPaymentErrorMsg2":"如已扣款:请联络会计组(电话:2231 5183 / 2231 5318)并提供交易信息。", + "fpsPaymentErrorMsg3":"如未扣款:请点击「返回主页」并等待30分钟后重新尝试。", "fpsSelectPaymentApp":"请选择付款支付程序", "payDnRemark": "在{date}下午12时30分前将付款证明(例如银行入数纸或网上银行付款记录)电邮至 gld_acct@gld.gov.hk", @@ -159,6 +161,7 @@ "iAmSmartNoIdNoMsg": "无效资料,请返回建立账户页面。", "mainPage": "主页", + "backToMainPage": "返回主页", "publicNotice": "公共启事", "publicNoticeApp": "公共啟事申请", "myPublicNotice": "我的公共启事", @@ -452,7 +455,7 @@ "payIdNRefer": "付款编号 / 付款参考编号", "payConfirm": "确认付款", "payCancel": "取消付款", - "payAlert": "请不要关闭此窗口,您可以通过此页面底部的按钮完成此付款或取消此付款。", + "payAlert": "付款过程中请勿重新整理或关闭此页面,以免支付失败。如需取消,请使用本页底部的「取消付款」按钮。", "payTotalDeatail": "付款总额", "payDeatail": "付款总额", "payTotal": "付款总额", @@ -486,7 +489,9 @@ "paymentLimitPrice2":"只适用于最小金额为 0.10 港元及最高金额为 9,999,999.99港元", "paymentLimitPPS":"付款不适用于流动装置的浏览器,请使用桌面电脑。", "paymentMethod": "付款方式", - "paymentProcessLimited":"请于15分钟内完成付款程序。 注意:使用转数快(FPS)时,因二维码具安全时效限制,须于3分钟内完成扫码、付款及所有相关审核程序。", + "paymentProcessLimited1":"请于15分钟内完成付款程序。", + "paymentProcessLimited2":"付款过程中请勿重新整理或关任何页面,以免支付失败。如需取消,请使用本页底部的「取消付款」按钮。", + "paymentProcessLimited3":"注意:使用转数快(FPS)时,因二维码具安全时效限制,须于3分钟内完成扫码、付款及所有相关审核程序。", "publicNoticeDetailTitle": "公共启事申请资料", "applyPerson": "申请人", diff --git a/src/translations/zh-HK.json b/src/translations/zh-HK.json index 64d2c124..9f18b5e2 100644 --- a/src/translations/zh-HK.json +++ b/src/translations/zh-HK.json @@ -90,7 +90,9 @@ "fpsQrcodeTitle4":"剩餘時間:", "fpsQrcodeTitle5":"秒", "fpsQrcodeExpired":"二維碼已過期", - "fpsPaymentErrorMsg":"載入支付二維碼時發生錯誤。請勿在付款過程中更新此頁面。如果付款未完成,請點擊「取消支付」按鈕並重新支付。由此造成的不便,敬請諒解。", + "fpsPaymentErrorMsg1":"頁面重新載入時發生錯誤,支付二維碼已失效。請先確認銀行交易紀錄:", + "fpsPaymentErrorMsg2":"如已扣款:請聯絡會計組(電話:2231 5183 / 2231 5318)並提供交易資料。", + "fpsPaymentErrorMsg3":"如未扣款:請點擊「返回主頁」並等待30分鐘後重新嘗試。", "fpsSelectPaymentApp":"請選擇付款支付程式", "payDnRemark": "在{date}下午12時30分前將付款證明(例如銀行入數紙或網上銀行付款記錄)電郵至 gld_acct@gld.gov.hk", @@ -159,6 +161,7 @@ "iAmSmartNoIdNoMsg": "無效資料,請返回建立賬戶頁面。", "mainPage": "主頁", + "backToMainPage": "返回主頁", "publicNotice": "公共啟事", "publicNoticeApp": "公共啟事申請", "myPublicNotice": "我的公共啟事", @@ -453,7 +456,7 @@ "payIdNRefer": "付款編號 / 付款參考編號", "payConfirm": "確認付款", "payCancel": "取消付款", - "payAlert": "請不要關閉此窗口,您可以透過本頁底部的按鈕完成此付款或取消本付款。", + "payAlert": "付款過程中請勿重新整理或關閉此頁面,以免支付失敗。如需取消,請使用本頁底部的「取消付款」按鈕。", "payTotalDeatail": "付款總額", "payDeatail": "付款總額", "payTotal": "付款總額", @@ -487,7 +490,9 @@ "paymentLimitPrice2":"只適用於最小金額為 0.10 港元及最高金額為 9,999,999.99港元", "paymentLimitPPS":"付款不適用於流動裝置的瀏覽器,請使用桌面電腦。", "paymentMethod": "付款方法", - "paymentProcessLimited":"請於15分鐘內完成付款程序。 注意:使用轉數快(FPS)時,因二維碼具安全時效限制,須於3分鐘內完成掃碼、付款及所有相關審核程序。", + "paymentProcessLimited1":"請於15分鐘內完成付款程序。", + "paymentProcessLimited2":"付款過程中請勿重新整理或關閉此頁面,以免支付失敗。如需取消,請使用本頁底部的「取消付款」按鈕。", + "paymentProcessLimited3":"注意:使用轉數快(FPS)時,因二維碼具安全時效限制,須於3分鐘內完成掃碼、付款及所有相關審核程序。", "publicNoticeDetailTitle": "公共啟事申請資料", "applyPerson": "申請人", From fdd595c5ce46485a8e764c3f0f18bd4919828052 Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Sat, 15 Aug 2026 12:21:38 +0800 Subject: [PATCH 03/22] CR023 - search proof support no paging. --- src/pages/Proof/Search_GLD/DataGrid.js | 1 - src/pages/Proof/Search_GLD/SearchForm.js | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/Proof/Search_GLD/DataGrid.js b/src/pages/Proof/Search_GLD/DataGrid.js index 7e2af177..e00e55db 100644 --- a/src/pages/Proof/Search_GLD/DataGrid.js +++ b/src/pages/Proof/Search_GLD/DataGrid.js @@ -146,7 +146,6 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead applyGridOnReady={applyGridOnReady} applySearch = {applySearch} serverSorting - disablePagingOnGazetteIssue={false} doLoad={React.useMemo(() => ({ url: LIST_PROOF, params: _searchCriteria, diff --git a/src/pages/Proof/Search_GLD/SearchForm.js b/src/pages/Proof/Search_GLD/SearchForm.js index 2c7b56e5..716d7532 100644 --- a/src/pages/Proof/Search_GLD/SearchForm.js +++ b/src/pages/Proof/Search_GLD/SearchForm.js @@ -161,6 +161,11 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o limit: 10, }; + if (issueSelected?.id) { + delete temp.start; + delete temp.limit; + } + if (searchCriteria?.sort && searchCriteria?.direction) { temp.sort = searchCriteria.sort; temp.direction = searchCriteria.direction; From 720fdbf9c4af98b96a8454ebca0f7e5956c03f16 Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Sat, 15 Aug 2026 12:48:07 +0800 Subject: [PATCH 04/22] update for CSP config --- docs/csp-apache.conf.md | 5 +- docs/csp-report-review-2026-08-03.md | 1 + docs/csp-report-review-2026-08-14.md | 212 +++++++++++++++++++++++++++ 3 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 docs/csp-report-review-2026-08-14.md diff --git a/docs/csp-apache.conf.md b/docs/csp-apache.conf.md index b712a4b7..fa0587c5 100644 --- a/docs/csp-apache.conf.md +++ b/docs/csp-apache.conf.md @@ -4,7 +4,10 @@ Self-hosted fonts: no `fonts.googleapis.com` or `fonts.gstatic.com`. Static asse Adjust `report-uri` if your API base path or host differs (UAT example below; PROD uses `https://pnsps.gld.gov.hk/api/csp-report`). -See also: [csp-report-review-2026-08-03.md](./csp-report-review-2026-08-03.md) for the PROD report analysis that drove `frame-src` / `media-src` updates. +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 +- [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 diff --git a/docs/csp-report-review-2026-08-03.md b/docs/csp-report-review-2026-08-03.md index 1e0aaea6..5200455c 100644 --- a/docs/csp-report-review-2026-08-03.md +++ b/docs/csp-report-review-2026-08-03.md @@ -12,6 +12,7 @@ Source logs (external): 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) --- diff --git a/docs/csp-report-review-2026-08-14.md b/docs/csp-report-review-2026-08-14.md new file mode 100644 index 00000000..34cad47f --- /dev/null +++ b/docs/csp-report-review-2026-08-14.md @@ -0,0 +1,212 @@ +# CSP Report Review (PROD + UAT, 2026-08-14) + +Review of Content-Security-Policy-Report-Only violations from PNSPS PROD +(`https://pnsps.gld.gov.hk`) and UAT (`https://pnspsuat.gld.gov.hk`), for +deciding Apache CSP updates. + +Source logs (external): + +- `P1 CSP.txt` — PROD node, 168 reports, 2026-07-30 to 2026-08-14 +- `P2 CSP.txt` — PROD node, 137 reports, 2026-07-30 to 2026-08-14 +- `TT CSP.txt` — UAT, 133 reports, 2026-05-29 to 2026-08-03 + +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 + +--- + +## Verdict + +CSP is still **Report-Only** (`disposition: report`) — nothing is blocking users +yet. There is **one real PNSPS SPA problem** (proof PDF preview `frame-src`). +Everything else is browser-extension noise or a **different app** on the same +UAT host. + +The 2026-08-03 recommendation to add `frame-src` is **still not on the live +header**. + +--- + +## Add / remove summary + +**PROD — add one line, remove nothing:** + +```apache +frame-src 'self' data: blob:; +``` + +Keep `script-src 'self'`. Do not add `'unsafe-inline'` or `'unsafe-eval'`. + +**UAT — add the same line, remove nothing now:** + +```apache +frame-src 'self' data: blob:; +``` + +Keep `script-src 'self' 'unsafe-inline' 'unsafe-eval'` for now (JSF payment recon +on the same host). Do not copy this `script-src` to PROD. + +**Do not add on either env:** `'wasm-unsafe-eval'`, Youdao, NetEase CDN, +`todesktop-internal`, Google Fonts. + +**Optional later (not required for these reports):** UAT can drop +`'unsafe-inline' 'unsafe-eval'` from the SPA header only after +`/paymentrecon-dept_web/` has its own CSP. + +--- + +## Confirmed current PROD policy (Report-Only) + +This is the live header that generated the P1/P2 reports. It matches the +`original-policy` in every PROD violation. + +```apache +Header always set Content-Security-Policy-Report-Only "default-src 'self'; \ + base-uri 'self'; \ + object-src 'none'; \ + frame-ancestors 'none'; \ + form-action 'self'; \ + script-src 'self'; \ + style-src 'self' 'unsafe-inline'; \ + style-src-elem 'self' 'unsafe-inline'; \ + img-src 'self' data: https://www.w3.org https://w3.org; \ + media-src 'self' blob:; \ + font-src 'self' data:; \ + connect-src 'self'; \ + upgrade-insecure-requests; \ + report-uri https://pnsps.gld.gov.hk/api/csp-report" +``` + +Gap vs the reports: **no `frame-src`**. Framing therefore falls back to +`default-src 'self'`, which is why proof PDF preview reports `frame-src` with +an empty `blocked-uri`. + +--- + +## Confirmed current UAT policy (Report-Only) + +```apache +Header always set Content-Security-Policy-Report-Only "default-src 'self'; \ + base-uri 'self'; \ + object-src 'none'; \ + frame-ancestors 'none'; \ + form-action 'self'; \ + script-src 'self' 'unsafe-inline' 'unsafe-eval'; \ + style-src 'self' 'unsafe-inline'; \ + style-src-elem 'self' 'unsafe-inline'; \ + img-src 'self' data: https://www.w3.org https://w3.org; \ + media-src 'self' blob:; \ + font-src 'self' data:; \ + connect-src 'self'; \ + upgrade-insecure-requests; \ + report-uri https://pnspsuat.gld.gov.hk/api/csp-report" +``` + +UAT vs PROD differences: + +- UAT `script-src` is `'self' 'unsafe-inline' 'unsafe-eval'` — PROD is `'self'` only. +- Both still lack `frame-src`. +- TT report `original-policy` was older: `script-src 'self' 'unsafe-inline'` + **without** `'unsafe-eval'`. That is why TT logged 42 JSF `eval` hits. Those + should stop under the current UAT header. + +Do **not** copy `'unsafe-inline'` or `'unsafe-eval'` onto PROD. The React SPA +does not need them. `'unsafe-eval'` is only there for the shared-host JSF +payment recon app. + +--- + +## Real problem (PROD P1 + P2) + +| Volume | Directive | Blocked | Pages | +|---:|---|---|---| +| 150 | `frame-src` | empty `""` | `/proof/create/{id}` | + +Source is always `static/js/4608.4af455e6.chunk.js`. Cause is proof file preview +in `src/pages/Proof/Create_FromApp/UploadFileTable.js`: +`FileReader.readAsDataURL` then `document.write` an `