From f35c51b28178aece05b158b51c1ae49f302ea348 Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Sat, 12 Sep 2026 04:15:51 +0800 Subject: [PATCH] CADD 2026 #17 fix XSS via raw html --- package-lock.json | 10 +++ package.json | 1 + src/components/SafeHtml.js | 17 ++++ src/pages/Message/Details/index.js | 3 +- src/pages/Payment/MultiPaymentWindow.js | 3 +- src/pages/Proof/Payment/Pay.js | 11 ++- src/pages/Proof/Payment/Pay_DN.js | 25 +++--- src/pages/Proof/Payment/Pay_Office.js | 37 ++++----- src/pages/Proof/Payment/Pay_Online.js | 23 +++--- src/pages/Proof/Reply_Public/ProofForm.js | 11 +-- .../ApplyForm/PublicNoticeApplyForm.js | 30 +++---- .../Details_Public/ApplicationDetailCard.js | 3 +- .../ListPanel/PendingPaymentTab.js | 3 +- src/pages/Setting/SystemSetting/Table.js | 3 +- src/pages/authentication/AuthWrapper.js | 33 ++++---- src/pages/authentication/RegisterCustom.js | 7 +- .../auth-forms/BusCustomFormWizard.js | 3 +- .../auth-forms/CustomFormWizard.js | 3 +- .../auth-forms/IAmSmartFormWizard.js | 3 +- src/pages/dashboard/Public/index.js | 3 +- src/pages/extra-pages/AboutUs/AboutUs_cn.js | 3 +- src/pages/extra-pages/AboutUs/AboutUs_en.js | 3 +- src/pages/extra-pages/AboutUs/AboutUs_zh.js | 3 +- .../extra-pages/ImportantNoticePage/index.js | 3 +- .../PrivacyPolicyPage/PrivacyPolicy_cn.js | 3 +- .../PrivacyPolicyPage/PrivacyPolicy_en.js | 4 +- .../PrivacyPolicyPage/PrivacyPolicy_zh.js | 4 +- .../extra-pages/PrivacyPolicyPage/index.js | 3 +- .../iAmSmart/PleaseLoginCallback/index.js | 3 +- src/pages/iAmSmart/SuccessCallback/index.js | 6 +- src/utils/sanitizeHtml.js | 78 +++++++++++++++++++ src/utils/sanitizeHtml.test.js | 53 +++++++++++++ yarn.lock | 14 +++- 33 files changed, 288 insertions(+), 124 deletions(-) create mode 100644 src/components/SafeHtml.js create mode 100644 src/utils/sanitizeHtml.js create mode 100644 src/utils/sanitizeHtml.test.js diff --git a/package-lock.json b/package-lock.json index 9a27f3b9..82370420 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "axios": "^1.12.2", "date-fns": "^3.0.6", "dayjs": "^1.11.9", + "dompurify": "^3.4.15", "formik": "^2.2.9", "framer-motion": "^7.3.6", "history": "^5.3.0", @@ -8419,6 +8420,15 @@ } ] }, + "node_modules/dompurify": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz", + "integrity": "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", diff --git a/package.json b/package.json index 3ee87f36..755e24e2 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "axios": "^1.12.2", "date-fns": "^3.0.6", "dayjs": "^1.11.9", + "dompurify": "^3.4.15", "formik": "^2.2.9", "framer-motion": "^7.3.6", "history": "^5.3.0", diff --git a/src/components/SafeHtml.js b/src/components/SafeHtml.js new file mode 100644 index 00000000..32f88aab --- /dev/null +++ b/src/components/SafeHtml.js @@ -0,0 +1,17 @@ +import PropTypes from 'prop-types'; +import { sanitizeHtml } from 'utils/sanitizeHtml'; + +/** + * Renders HTML after {@link sanitizeHtml}. Use instead of raw + * dangerouslySetInnerHTML for messages, settings, CMS, and i18n HTML. + */ +const SafeHtml = ({ html, component: Component = 'div', ...rest }) => { + return ; +}; + +SafeHtml.propTypes = { + html: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + component: PropTypes.elementType +}; + +export default SafeHtml; diff --git a/src/pages/Message/Details/index.js b/src/pages/Message/Details/index.js index f3f4c5cb..c6639902 100644 --- a/src/pages/Message/Details/index.js +++ b/src/pages/Message/Details/index.js @@ -19,6 +19,7 @@ import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import { FormattedMessage } from "react-intl"; import usePageTitle from 'components/usePageTitle'; import { PRIMARY_CONTAINED_BUTTON_SX } from 'themes/colorConst'; +import SafeHtml from 'components/SafeHtml'; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, @@ -93,7 +94,7 @@ const Index = () => { {DateUtils.datetimeStr(record?.sentDate)} -
+ diff --git a/src/pages/Payment/MultiPaymentWindow.js b/src/pages/Payment/MultiPaymentWindow.js index f1bbcef9..a7f4799d 100644 --- a/src/pages/Payment/MultiPaymentWindow.js +++ b/src/pages/Payment/MultiPaymentWindow.js @@ -25,6 +25,7 @@ import Loadable from 'components/Loadable'; const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); import * as FormatUtils from "utils/FormatUtils"; +import SafeHtml from 'components/SafeHtml'; import VisaIcon from "assets/images/icons/visacard.svg"; import MasterIcon from "assets/images/icons/mastercard.svg"; import JcbIcon from "assets/images/icons/jcb.svg"; @@ -500,7 +501,7 @@ const MultiPaymentWindow = (props) => { -
+ diff --git a/src/pages/Proof/Payment/Pay.js b/src/pages/Proof/Payment/Pay.js index ad673edf..0de0b344 100644 --- a/src/pages/Proof/Payment/Pay.js +++ b/src/pages/Proof/Payment/Pay.js @@ -28,6 +28,7 @@ const BackgroundHead = { import { PNSPS_BUTTON_THEME, PNSPS_LONG_BUTTON_THEME } from "../../../themes/buttonConst"; import { ThemeProvider } from "@emotion/react"; import { FormattedMessage, useIntl } from "react-intl"; +import SafeHtml from 'components/SafeHtml'; // ==============================|| DASHBOARD - DEFAULT ||============================== // const Index = ({ record }) => { @@ -107,7 +108,7 @@ const Index = ({ record }) => { -
+
@@ -116,14 +117,12 @@ const Index = ({ record }) => {

-
+ })} /> @@ -213,7 +212,7 @@ const Index = ({ record }) => { -
+ diff --git a/src/pages/Proof/Payment/Pay_DN.js b/src/pages/Proof/Payment/Pay_DN.js index fe40882e..8a92d8aa 100644 --- a/src/pages/Proof/Payment/Pay_DN.js +++ b/src/pages/Proof/Payment/Pay_DN.js @@ -14,6 +14,7 @@ import * as DateUtils from "utils/DateUtils"; import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import { FormattedMessage, useIntl } from "react-intl"; +import SafeHtml from 'components/SafeHtml'; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, width: '100%', @@ -76,19 +77,16 @@ const isAfter = checkIsOnlyOnlinePayment(); )} -
+ )} />
-
+ )} />
-
+ )} />
-
+ )} /> diff --git a/src/pages/Proof/Payment/Pay_Office.js b/src/pages/Proof/Payment/Pay_Office.js index 1c201051..9eeefdb8 100644 --- a/src/pages/Proof/Payment/Pay_Office.js +++ b/src/pages/Proof/Payment/Pay_Office.js @@ -14,6 +14,7 @@ import * as DateUtils from "utils/DateUtils"; import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import { FormattedMessage, useIntl } from "react-intl"; +import SafeHtml from 'components/SafeHtml'; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, width: '100%', @@ -78,59 +79,47 @@ const isAfter = checkIsOnlyOnlinePayment(); -
+ )} />
-
+ )} />
-
+ )} />
-
+ )} />
-
+ )} />
-
+ )} /> diff --git a/src/pages/Proof/Payment/Pay_Online.js b/src/pages/Proof/Payment/Pay_Online.js index 33582994..93684400 100644 --- a/src/pages/Proof/Payment/Pay_Online.js +++ b/src/pages/Proof/Payment/Pay_Online.js @@ -22,6 +22,7 @@ import Loadable from 'components/Loadable'; const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' +import SafeHtml from 'components/SafeHtml'; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, width: '100%', @@ -195,32 +196,28 @@ const Index = () => { )} -
+ )} />
-
+ )} />
{checkPaymentSuspension()?
-
+
:null @@ -272,13 +269,11 @@ const Index = () => { } -
+ )} />
@@ -338,7 +333,7 @@ const Index = () => { -
+ diff --git a/src/pages/Proof/Reply_Public/ProofForm.js b/src/pages/Proof/Reply_Public/ProofForm.js index 56783bf3..a994122c 100644 --- a/src/pages/Proof/Reply_Public/ProofForm.js +++ b/src/pages/Proof/Reply_Public/ProofForm.js @@ -23,6 +23,7 @@ import { useNavigate } from "react-router-dom"; import * as DateUtils from "utils/DateUtils" import Loadable from 'components/Loadable'; import { notifyActionSuccess } from 'utils/CommonFunction'; +import SafeHtml from 'components/SafeHtml'; import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; import { ThemeProvider } from "@emotion/react"; @@ -382,7 +383,7 @@ const FormPanel = ({ formData }) => {
  • -
    + ); @@ -468,7 +469,7 @@ const FormPanel = ({ formData }) => { {checkPaymentSuspension()? -
    + :null }
    { @@ -536,7 +537,7 @@ const FormPanel = ({ formData }) => {
  • -
    + ); @@ -599,13 +600,13 @@ const FormPanel = ({ formData }) => { } {/* -
    + */} {!isOnlyOnlinePayment? -
    + :null } diff --git a/src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js b/src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js index c56db966..04e96f19 100644 --- a/src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js +++ b/src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js @@ -34,6 +34,7 @@ import { FormattedMessage, useIntl } from "react-intl"; import { useState, useEffect, useRef } from 'react'; import { checkPaymentSuspension } from "utils/Utils"; import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; +import SafeHtml from 'components/SafeHtml'; // ==============================|| DASHBOARD - DEFAULT ||============================== // @@ -409,13 +410,12 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => ml:2 }} > - )} @@ -459,7 +459,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) =>
  • -
    + ); @@ -603,14 +603,14 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => - +
      -
    1. -
    2. -
    3. -
    4. + + + +
    @@ -630,7 +630,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => }} /> -
    + @@ -653,7 +653,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => -
    + diff --git a/src/pages/PublicNotice/Details_Public/ApplicationDetailCard.js b/src/pages/PublicNotice/Details_Public/ApplicationDetailCard.js index e8b50064..bb9404f2 100644 --- a/src/pages/PublicNotice/Details_Public/ApplicationDetailCard.js +++ b/src/pages/PublicNotice/Details_Public/ApplicationDetailCard.js @@ -41,6 +41,7 @@ import EditNoteIcon from '@mui/icons-material/EditNote'; import DownloadIcon from '@mui/icons-material/Download'; import { PNSPS_BUTTON_THEME } from "../../../themes/buttonConst"; import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; +import SafeHtml from 'components/SafeHtml'; import { ThemeProvider } from "@emotion/react"; import { FormattedMessage, useIntl } from "react-intl"; // ==============================|| DASHBOARD - DEFAULT ||============================== // @@ -834,7 +835,7 @@ const ApplicationDetailCard = ( -
    + diff --git a/src/pages/PublicNotice/ListPanel/PendingPaymentTab.js b/src/pages/PublicNotice/ListPanel/PendingPaymentTab.js index af6677f6..859ad5c4 100644 --- a/src/pages/PublicNotice/ListPanel/PendingPaymentTab.js +++ b/src/pages/PublicNotice/ListPanel/PendingPaymentTab.js @@ -28,6 +28,7 @@ import { // import { dateStr } from "utils/DateUtils"; import { ThemeProvider, useTheme } from "@emotion/react"; import { PNSPS_BUTTON_THEME } from "../../../themes/buttonConst"; +import SafeHtml from 'components/SafeHtml'; import { FormattedMessage, useIntl } from "react-intl"; import { PRIMARY_TEXT_BUTTON_SX, PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; // ==============================|| EVENT TABLE ||============================== // @@ -580,7 +581,7 @@ export default function SubmittedTab({ setCount, url }) { -
    + diff --git a/src/pages/Setting/SystemSetting/Table.js b/src/pages/Setting/SystemSetting/Table.js index 8d4f966e..6b43df4b 100644 --- a/src/pages/Setting/SystemSetting/Table.js +++ b/src/pages/Setting/SystemSetting/Table.js @@ -7,6 +7,7 @@ import MainCard from "components/MainCard"; import * as React from "react"; import { FiDataGrid } from "components/FiDataGrid"; import { GET_SYS_PARAMS } from "utils/ApiPathConst"; +import SafeHtml from 'components/SafeHtml'; // ==============================|| DASHBOARD - DEFAULT ||============================== // @@ -45,7 +46,7 @@ const Table = ({onRowClick, searchCriteria, refreshTrigger}) => { flex: 1, minWidth: 400, renderCell:(params)=>{ - return
    + return } }, ]; diff --git a/src/pages/authentication/AuthWrapper.js b/src/pages/authentication/AuthWrapper.js index 41dba56a..9d282b94 100644 --- a/src/pages/authentication/AuthWrapper.js +++ b/src/pages/authentication/AuthWrapper.js @@ -10,6 +10,7 @@ import backbroundImg from 'assets/images/bg_ml.jpg'; import lgceImg from 'assets/images/2025_lgce.jpg'; import 'assets/style/loginStyles.css'; import { SysContext } from 'components/SysSettingProvider'; +import SafeHtml from 'components/SafeHtml'; const AuthCard = Loadable(lazy(() => import('./AuthCardCustom'))); const BackgroundHead = { @@ -175,11 +176,9 @@ const AuthWrapper = ({ children }) => { '& p': { marginBottom: '12px', lineHeight: 1.7 } }} > - + + + @@ -207,24 +206,18 @@ const AuthWrapper = ({ children }) => { > {checkPaymentSuspension() ? ( - + + + ) : ( - + + })} + /> + )} diff --git a/src/pages/authentication/RegisterCustom.js b/src/pages/authentication/RegisterCustom.js index 1e5702fa..14e14885 100644 --- a/src/pages/authentication/RegisterCustom.js +++ b/src/pages/authentication/RegisterCustom.js @@ -17,6 +17,7 @@ import { I_AM_SMART_PATH } from "utils/ApiPathConst"; import * as React from 'react'; import { FormattedMessage, useIntl } from "react-intl"; import usePageTitle from "components/usePageTitle"; +import SafeHtml from 'components/SafeHtml'; // ================================|| LOGIN ||================================ // @@ -127,7 +128,7 @@ const RegisterCustom = () => { /> -
    + @@ -160,7 +161,7 @@ const RegisterCustom = () => { -
    + @@ -189,7 +190,7 @@ const RegisterCustom = () => { -
    + diff --git a/src/pages/authentication/auth-forms/BusCustomFormWizard.js b/src/pages/authentication/auth-forms/BusCustomFormWizard.js index 77e8371c..458751ac 100644 --- a/src/pages/authentication/auth-forms/BusCustomFormWizard.js +++ b/src/pages/authentication/auth-forms/BusCustomFormWizard.js @@ -35,6 +35,7 @@ import { POST_PUBLIC_USER_REGISTER, POST_CAPTCHA, POST_USERNAME, POST_USER_EMAIL import * as ComboData from "utils/ComboData"; import Loadable from 'components/Loadable'; +import SafeHtml from 'components/SafeHtml'; import { lazy } from 'react'; const UploadFileTable = Loadable(lazy(() => import('./UploadFileTable'))); const LoadingComponent = Loadable(lazy(() => import('../../extra-pages/LoadingComponent'))); @@ -1618,7 +1619,7 @@ const BusCustomFormWizard = (props) => { -
    + diff --git a/src/pages/authentication/auth-forms/CustomFormWizard.js b/src/pages/authentication/auth-forms/CustomFormWizard.js index 5d522f84..931a5224 100644 --- a/src/pages/authentication/auth-forms/CustomFormWizard.js +++ b/src/pages/authentication/auth-forms/CustomFormWizard.js @@ -28,6 +28,7 @@ import { POST_USERNAME, POST_USER_EMAIL, POST_CAPTCHA, POST_PUBLIC_USER_REGISTER import * as ComboData from "utils/ComboData"; import Loadable from 'components/Loadable'; +import SafeHtml from 'components/SafeHtml'; import { lazy } from 'react'; const UploadFileTable = Loadable(lazy(() => import('./UploadFileTable'))); const PreviewUploadFileTable = Loadable(lazy(() => import('./PreviewUploadFileTable'))); @@ -1946,7 +1947,7 @@ const CustomFormWizard = (props) => { -
    + diff --git a/src/pages/authentication/auth-forms/IAmSmartFormWizard.js b/src/pages/authentication/auth-forms/IAmSmartFormWizard.js index a84714f6..fc3919fa 100644 --- a/src/pages/authentication/auth-forms/IAmSmartFormWizard.js +++ b/src/pages/authentication/auth-forms/IAmSmartFormWizard.js @@ -26,6 +26,7 @@ import { POST_IAMSMART_USER_REGISTER, POST_CAPTCHA, POST_USER_EMAIL, POST_CAPTCH import * as ComboData from "utils/ComboData"; import Loadable from 'components/Loadable'; +import SafeHtml from 'components/SafeHtml'; import { lazy } from 'react'; const LoadingComponent = Loadable(lazy(() => import('../../extra-pages/LoadingComponent'))); @@ -1055,7 +1056,7 @@ const CustomFormWizard = (props) => { -
    + diff --git a/src/pages/dashboard/Public/index.js b/src/pages/dashboard/Public/index.js index fad79e47..e5450461 100644 --- a/src/pages/dashboard/Public/index.js +++ b/src/pages/dashboard/Public/index.js @@ -29,6 +29,7 @@ const LoadingComponent = Loadable(React.lazy(() => import('../../extra-pages/Loa import { useNavigate } from "react-router-dom"; import usePageTitle from "components/usePageTitle"; +import SafeHtml from 'components/SafeHtml'; // ==============================|| DASHBOARD - DEFAULT ||============================== // @@ -297,7 +298,7 @@ const DashboardDefault = () => { -
    + diff --git a/src/pages/extra-pages/AboutUs/AboutUs_cn.js b/src/pages/extra-pages/AboutUs/AboutUs_cn.js index de5d2fd1..7f16b740 100644 --- a/src/pages/extra-pages/AboutUs/AboutUs_cn.js +++ b/src/pages/extra-pages/AboutUs/AboutUs_cn.js @@ -1,3 +1,4 @@ +import SafeHtml from 'components/SafeHtml'; const AboutUs = () => { @@ -155,7 +156,7 @@ const AboutUs = () => { ` ; - return (
    ); + return ; } diff --git a/src/pages/extra-pages/AboutUs/AboutUs_en.js b/src/pages/extra-pages/AboutUs/AboutUs_en.js index b026ad8a..c6a56e45 100644 --- a/src/pages/extra-pages/AboutUs/AboutUs_en.js +++ b/src/pages/extra-pages/AboutUs/AboutUs_en.js @@ -1,3 +1,4 @@ +import SafeHtml from 'components/SafeHtml'; const AboutUs = () => { @@ -162,7 +163,7 @@ const AboutUs = () => {
    ` ; - return (
    ); + return ; } diff --git a/src/pages/extra-pages/AboutUs/AboutUs_zh.js b/src/pages/extra-pages/AboutUs/AboutUs_zh.js index c7600cab..8e259c87 100644 --- a/src/pages/extra-pages/AboutUs/AboutUs_zh.js +++ b/src/pages/extra-pages/AboutUs/AboutUs_zh.js @@ -1,3 +1,4 @@ +import SafeHtml from 'components/SafeHtml'; const AboutUs = () => { @@ -165,7 +166,7 @@ const AboutUs = () => { ` ; - return (
    ); + return ; } diff --git a/src/pages/extra-pages/ImportantNoticePage/index.js b/src/pages/extra-pages/ImportantNoticePage/index.js index 65ee7286..2936417f 100644 --- a/src/pages/extra-pages/ImportantNoticePage/index.js +++ b/src/pages/extra-pages/ImportantNoticePage/index.js @@ -3,6 +3,7 @@ import { useIntl, FormattedMessage } from 'react-intl'; import usePageTitle from "components/usePageTitle"; import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'; +import SafeHtml from 'components/SafeHtml'; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, @@ -38,7 +39,7 @@ const ImportantNotice = () => {
    -
    + ); diff --git a/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_cn.js b/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_cn.js index 6ea9c981..999a7ab0 100644 --- a/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_cn.js +++ b/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_cn.js @@ -1,3 +1,4 @@ +import SafeHtml from 'components/SafeHtml'; const Page = () => { @@ -83,7 +84,7 @@ const Page = () => {
    `; - return (
    ); + return ; } diff --git a/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js b/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js index d5b94997..9dce9342 100644 --- a/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js +++ b/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js @@ -1,3 +1,5 @@ +import SafeHtml from 'components/SafeHtml'; + const Page = () => { const content = `