瀏覽代碼

CADD 2026 #17 fix XSS via raw html

CR023
Jason Chuang 1 周之前
父節點
當前提交
f35c51b281
共有 33 個檔案被更改,包括 288 行新增124 行删除
  1. +10
    -0
      package-lock.json
  2. +1
    -0
      package.json
  3. +17
    -0
      src/components/SafeHtml.js
  4. +2
    -1
      src/pages/Message/Details/index.js
  5. +2
    -1
      src/pages/Payment/MultiPaymentWindow.js
  6. +5
    -6
      src/pages/Proof/Payment/Pay.js
  7. +9
    -16
      src/pages/Proof/Payment/Pay_DN.js
  8. +13
    -24
      src/pages/Proof/Payment/Pay_Office.js
  9. +9
    -14
      src/pages/Proof/Payment/Pay_Online.js
  10. +6
    -5
      src/pages/Proof/Reply_Public/ProofForm.js
  11. +15
    -15
      src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js
  12. +2
    -1
      src/pages/PublicNotice/Details_Public/ApplicationDetailCard.js
  13. +2
    -1
      src/pages/PublicNotice/ListPanel/PendingPaymentTab.js
  14. +2
    -1
      src/pages/Setting/SystemSetting/Table.js
  15. +13
    -20
      src/pages/authentication/AuthWrapper.js
  16. +4
    -3
      src/pages/authentication/RegisterCustom.js
  17. +2
    -1
      src/pages/authentication/auth-forms/BusCustomFormWizard.js
  18. +2
    -1
      src/pages/authentication/auth-forms/CustomFormWizard.js
  19. +2
    -1
      src/pages/authentication/auth-forms/IAmSmartFormWizard.js
  20. +2
    -1
      src/pages/dashboard/Public/index.js
  21. +2
    -1
      src/pages/extra-pages/AboutUs/AboutUs_cn.js
  22. +2
    -1
      src/pages/extra-pages/AboutUs/AboutUs_en.js
  23. +2
    -1
      src/pages/extra-pages/AboutUs/AboutUs_zh.js
  24. +2
    -1
      src/pages/extra-pages/ImportantNoticePage/index.js
  25. +2
    -1
      src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_cn.js
  26. +3
    -1
      src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js
  27. +3
    -1
      src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_zh.js
  28. +2
    -1
      src/pages/extra-pages/PrivacyPolicyPage/index.js
  29. +2
    -1
      src/pages/iAmSmart/PleaseLoginCallback/index.js
  30. +4
    -2
      src/pages/iAmSmart/SuccessCallback/index.js
  31. +78
    -0
      src/utils/sanitizeHtml.js
  32. +53
    -0
      src/utils/sanitizeHtml.test.js
  33. +13
    -1
      yarn.lock

+ 10
- 0
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",


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


+ 17
- 0
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 <Component {...rest} dangerouslySetInnerHTML={{ __html: sanitizeHtml(html) }} />;
};

SafeHtml.propTypes = {
html: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
component: PropTypes.elementType
};

export default SafeHtml;

+ 2
- 1
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 = () => {
</Typography>
<Typography sx={{p:1}} align="justify">{DateUtils.datetimeStr(record?.sentDate)}</Typography>
<Typography component="div" variant="body1" sx={{ p: 4, textAlign: "left", bgcolor: "#f8f8f8" }} align="justify">
<div dangerouslySetInnerHTML={{ __html: record?.content }} />
<SafeHtml html={record?.content} />
</Typography>

<Typography component="h3" variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "center" }}>


+ 2
- 1
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) => {
<Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography>
<DialogContent style={{ display: 'flex', }}>
<Stack direction="column" justifyContent="space-between">
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} />
</Stack>
</DialogContent>
<DialogActions>


+ 5
- 6
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 }) => {
</Typography>

<Typography variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.payMsg1' }, { appNo: record.appNo }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.payMsg1' }, { appNo: record.appNo })} />
<br />
<FormattedMessage id="MSG.payMsg2_1" />
<span style={{ color: "red" }}>
@@ -116,14 +117,12 @@ const Index = ({ record }) => {
<FormattedMessage id="MSG.payMsg2_2" />
<br />
<br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage({ id: 'MSG.payMsg3' },
<SafeHtml html={intl.formatMessage({ id: 'MSG.payMsg3' },
{
issueYear: record?.issueYear,
issueVolume: record?.issueVolume,
issueNo: record?.issueNo,
})
}} />
})} />
</Typography>

<Typography variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}>
@@ -213,7 +212,7 @@ const Index = ({ record }) => {
<Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography>
<DialogContent style={{ display: 'flex', }}>
<Stack direction="column" justifyContent="space-between">
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} />
</Stack>
</DialogContent>
<DialogActions>


+ 9
- 16
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();
)}

<Typography variant="h5" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}>
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{
id: 'proofPaymentBody_demandNote'
},
{
appNo: record?.appNo,
}
)
}} />
)} />
<br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{
id: 'proofPaymentBody_demandNote2'
},
@@ -96,27 +94,22 @@ const isAfter = checkIsOnlyOnlinePayment();
closingDateOff: DateUtils.dateFormat(record?.closingDateOff, intl.formatMessage({id: "dateStrFormat"})),
email: record?.mail,
}
)
}} />
)} />
<br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{
id: 'proofPaymentBody_demandNote3'
},
{
paymentDeadline: DateUtils.dateFormat(record?.closingDate, intl.formatMessage({id: "dateStrFormat"})),
}
)
}} />
)} />
<br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{
id: 'proofPaymentBody_demandNote4'
}
)
}} />
)} />


</Typography>


+ 13
- 24
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();


<Typography variant="h5" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}>
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{
id: 'proofPaymentBody_office'
},
{
appNo: record?.appNo,
}
)
}} />
)} />
<br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{
id: 'proofPaymentBody_office2'
},
)
}} />
)} />
<br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{
id: 'proofPaymentBody_office3'
}
)
}} />
)} />
<br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{
id: 'proofPaymentBody_office4'
}
)
}} />
)} />
<br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{
id: 'proofPaymentBody_office5'
},
{
paymentDeadline: DateUtils.dateFormat(record?.closingDate, intl.formatMessage({id: "dateStrFormat"})),
}
)
}} />
)} />
<br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{
id: 'proofPaymentBody_office6'
}
)
}} />
)} />


</Typography>


+ 9
- 14
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 = () => {
)}

<Typography variant="h5" sx={bodySx}>
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{
id: 'proofPaymentBody_online'
},
{
appNo: record?.appNo,
}
)
}} />
)} />
<br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{
id: 'proofPaymentBody_online2'
},
{
paymentDeadline: DateUtils.formatDateForLocale(record?.expiryDate, intl, locale),
}
)
}} />
)} />
<br />
{checkPaymentSuspension()?
<div>
<Typography style={{ textAlign: "flex-start", color: "red" }}>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "suspensionMessageText" }) }} />
<SafeHtml html={intl.formatMessage({ id: "suspensionMessageText" })} />
</Typography>
<br />
</div>:null
@@ -272,13 +269,11 @@ const Index = () => {
}

<Typography variant="h4" sx={bodySx}>
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{
id: 'proofPaymentBody_online3'
}
)
}} />
)} />
<br />
</Typography>

@@ -338,7 +333,7 @@ const Index = () => {
<Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography>
<DialogContent style={{ display: 'flex', }}>
<Stack direction="column" justifyContent="space-between">
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} />
</Stack>
</DialogContent>
<DialogActions>


+ 6
- 5
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 }) => {
<li><FormattedMessage id="post" /></li>
</ul>
<Typography variant="h6" component="span">
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} />
<SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} />
</Typography>
</>
);
@@ -468,7 +469,7 @@ const FormPanel = ({ formData }) => {
<FormattedMessage id="payOnline" />
{checkPaymentSuspension()?
<Typography style={{ padding: '16px', color: "red" }}>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "suspensionMessageText" }) }} />
<SafeHtml html={intl.formatMessage({ id: "suspensionMessageText" })} />
</Typography>:null
}
<br /><a href="#payOnlineDetails" color='#fff' onClick={() => {
@@ -536,7 +537,7 @@ const FormPanel = ({ formData }) => {
<li><FormattedMessage id="post" /></li>
</ul>
<Typography variant="h6" component="span">
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} />
<SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} />
</Typography>
</>
);
@@ -599,13 +600,13 @@ const FormPanel = ({ formData }) => {
}
{/* <Grid item xs={12}>
<Typography variant="h6" component="span" height="100%" >
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} />
<SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} />
</Typography>
</Grid> */}
{!isOnlyOnlinePayment?
<Grid item xs={12}>
<Typography variant="h6" component="span" height="100%" >
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofImportant" }) }} />
<SafeHtml html={intl.formatMessage({ id: "proofImportant" })} style={{ padding: 12 }} />
</Typography>
</Grid>:null
}


+ 15
- 15
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
}}
>
<span
dangerouslySetInnerHTML={{
__html: intl.formatMessage({
id: "suspensionMessageText",
defaultMessage: ""
})
}}
<SafeHtml
component="span"
html={intl.formatMessage({
id: "suspensionMessageText",
defaultMessage: ""
})}
/>
</Typography>
)}
@@ -459,7 +459,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) =>
<li><FormattedMessage id="post" /></li>
</ul>
<Typography variant="h6" component="span">
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} />
<SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} />
</Typography>
</>
);
@@ -603,14 +603,14 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) =>

<Grid item xs={12} mr={1} mb={2}>
<Typography variant="pnspsFormParagraphBold">
<span style={{ textAlign: 'justify', }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr0" }) }} />
<SafeHtml component="span" style={{ textAlign: 'justify', }} html={intl.formatMessage({ id: "applyTickUnderStr0" })} />
</Typography>
<Typography display="inline" variant="subtitle1" component="span" sx={{ color: 'primary.primary' }} >
<ol style={{ textAlign: 'justify', }}>
<li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr1" }) }} />
<li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr2" }) }} />
<li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr3" }) }} />
<li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "tradeMarkFootnote" }) }} />
<SafeHtml component="li" html={intl.formatMessage({ id: "applyTickUnderStr1" })} />
<SafeHtml component="li" html={intl.formatMessage({ id: "applyTickUnderStr2" })} />
<SafeHtml component="li" html={intl.formatMessage({ id: "applyTickUnderStr3" })} />
<SafeHtml component="li" html={intl.formatMessage({ id: "tradeMarkFootnote" })} />
</ol>
</Typography>
</Grid>
@@ -630,7 +630,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) =>
}}
/>
<Typography variant="h6" component="span" height="100%" >
<div style={{ padding: 12, textAlign: 'justify' }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickStr" }) }} />
<SafeHtml html={intl.formatMessage({ id: "applyTickStr" })} style={{ padding: 12, textAlign: 'justify' }} />
</Typography>
</Stack>
</Grid>
@@ -653,7 +653,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) =>

<Grid item xs={12}>
<Typography variant="h6" component="span" height="100%" >
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyPublicNoticeText" }) }} />
<SafeHtml html={intl.formatMessage({ id: "applyPublicNoticeText" })} style={{ padding: 12 }} />
</Typography>
</Grid>



+ 2
- 1
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 = (
<Typography variant="h4" component="span" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography>
<DialogContent style={{ display: 'flex', }}>
<Stack direction="column" justifyContent="space-between">
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} />
</Stack>
</DialogContent>
<DialogActions>


+ 2
- 1
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 }) {
<Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography>
<DialogContent style={{ display: 'flex', }}>
<Stack direction="column" justifyContent="space-between">
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} />
</Stack>
</DialogContent>
<DialogActions>


+ 2
- 1
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 <div dangerouslySetInnerHTML={{__html: params.value}} />
return <SafeHtml html={params.value} />
}
},
];


+ 13
- 20
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 }
}}
>
<Typography
component="div"
sx={{ fontSize: 18, lineHeight: 1.8 }}
dangerouslySetInnerHTML={{ __html: popupHtml }}
/>
<Typography component="div" sx={{ fontSize: 18, lineHeight: 1.8 }}>
<SafeHtml html={popupHtml} />
</Typography>
</DialogContent>
</Dialog>

@@ -207,24 +206,18 @@ const AuthWrapper = ({ children }) => {
>
<Grid item xs={12} sx={{ px: { xs: 2 }, pl: { md: 4 }, pt: { xs: 4, sm: 2, md: 1.5 }, pb: { xs: 4, sm: 0 } }}>
{checkPaymentSuspension() ? (
<Typography
component="div"
sx={{ color: 'error.main', textAlign: 'start', p: 1.5 }}
dangerouslySetInnerHTML={{
__html: intl.formatMessage({ id: 'suspensionMessageText', defaultMessage: '' })
}}
/>
<Typography component="div" sx={{ color: 'error.main', textAlign: 'start', p: 1.5 }}>
<SafeHtml html={intl.formatMessage({ id: 'suspensionMessageText', defaultMessage: '' })} />
</Typography>
) : (
<Typography
component="div"
sx={{ textAlign: 'start', p: 1.5 }}
dangerouslySetInnerHTML={{
__html: intl.formatMessage({
<Typography component="div" sx={{ textAlign: 'start', p: 1.5 }}>
<SafeHtml
html={intl.formatMessage({
id: isOnlyOnline ? 'landingMessage' : 'homePageHeaderMessage',
defaultMessage: ''
})
}}
/>
})}
/>
</Typography>
)}
</Grid>



+ 4
- 3
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 = () => {
/>
<Box mt={4} ml={2} mr={2} bgcolor="grey.100" p={1.5} >
<Typography textAlign='justify' variant="body1" display="block" gutterBottom>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.registerIAmSmart' }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.registerIAmSmart' })} />
</Typography>
<Link href={intl.formatMessage({ id: "iamsmartLink" })}>
<FormattedMessage id="learnMore" />
@@ -160,7 +161,7 @@ const RegisterCustom = () => {
</Button>

<Typography ml={4} mr={4} mt={4} variant="body1" display="block" sx={{ fontWeight: 'bold' }} gutterBottom>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.registerPersonal' }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.registerPersonal' })} />
</Typography>
</Grid>
<Grid item xs={12} md={6} sx={{ borderLeft: 1, borderColor: 'grey.500' }}>
@@ -189,7 +190,7 @@ const RegisterCustom = () => {
</Typography>
</Button>
<Typography ml={4} mr={4} mt={4} variant="body1" display="block" sx={{ fontWeight: 'bold' }} gutterBottom>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.registerOrg' }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.registerOrg' })} />
</Typography>
</Grid>
</Grid>


+ 2
- 1
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) => {
<Grid container>
<Grid item xs={12} md={12}>
<Typography component="span" variant="h6" height="100%" sx={{ textAlign: "left", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e", display: "block" }}>
<div style={{padding: 12}} dangerouslySetInnerHTML={{__html: intl.formatMessage({id: "termsAndCon"})}} />
<SafeHtml html={intl.formatMessage({id: "termsAndCon"})} style={{padding: 12}} />
</Typography>
</Grid>
</Grid>


+ 2
- 1
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) => {
<Grid container>
<Grid item xs={12} md={12}>
<Typography height="100%" component="span" sx={{ textAlign: "left", display: "block", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}>
<div style={{padding: 12}} dangerouslySetInnerHTML={{__html: intl.formatMessage({id: "termsAndCon"})}} />
<SafeHtml html={intl.formatMessage({id: "termsAndCon"})} style={{padding: 12}} />
</Typography>
</Grid>
</Grid>


+ 2
- 1
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) => {
<Grid item xs={12} md={12}>
<Typography component="span" sx={{ textAlign: "left", display: "block", borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}>
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "termsAndCon" }) }} />
<SafeHtml html={intl.formatMessage({ id: "termsAndCon" })} style={{ padding: 12 }} />
</Typography>
</Grid>
</Grid>


+ 2
- 1
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 = () => {
</DialogTitle>
<DialogContent style={{ color: '#B00020', display: 'flex', }}>
<Typography variant="h5" style={{ padding: '16px' }}>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "suspensionMessageText" }) }} />
<SafeHtml html={intl.formatMessage({ id: "suspensionMessageText" })} />
</Typography>
</DialogContent>
<DialogActions>


+ 2
- 1
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 (<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />);
return <SafeHtml html={content} style={{ padding: 12 }} />;

}


+ 2
- 1
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 = () => {
</div>`
;

return (<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />);
return <SafeHtml html={content} style={{ padding: 12 }} />;

}


+ 2
- 1
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 (<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />);
return <SafeHtml html={content} style={{ padding: 12 }} />;

}


+ 2
- 1
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 = () => {
</div>
</Grid>
<Grid item xs={10} md={8} lg={6}>
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: htmlContent }} />
<SafeHtml html={htmlContent} style={{ padding: 12 }} />
</Grid>
</Grid>
);


+ 2
- 1
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 = () => {
</div>
`;

return (<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />);
return <SafeHtml html={content} style={{ padding: 12 }} />;

}


+ 3
- 1
src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js 查看文件

@@ -1,3 +1,5 @@
import SafeHtml from 'components/SafeHtml';

const Page = () => {
const content = `
<style>
@@ -125,7 +127,7 @@ const Page = () => {
</div>
`;

return <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />;
return <SafeHtml html={content} style={{ padding: 12 }} />;
};

export default Page;

+ 3
- 1
src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_zh.js 查看文件

@@ -1,3 +1,5 @@
import SafeHtml from 'components/SafeHtml';

const Page = () => {
const content = `
<style>
@@ -98,7 +100,7 @@ const Page = () => {
</div>
`;

return <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />;
return <SafeHtml html={content} style={{ padding: 12 }} />;
};

export default Page;

+ 2
- 1
src/pages/extra-pages/PrivacyPolicyPage/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 PrivacyPolicy = () => {
</div>
</Grid>
<Grid item xs={10} md={8} lg={6}>
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: htmlContent }} />
<SafeHtml html={htmlContent} style={{ padding: 12 }} />
</Grid>
</Grid>
);


+ 2
- 1
src/pages/iAmSmart/PleaseLoginCallback/index.js 查看文件

@@ -12,6 +12,7 @@ import { useNavigate } from "react-router-dom";
import Loadable from 'components/Loadable';
const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent')));
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import SafeHtml from 'components/SafeHtml';

import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'
const BackgroundHead = {
@@ -66,7 +67,7 @@ const Index = () => {
<AccountCircleIcon color="secondary" sx={{ width: "200px", height: "200px" }} />
<Grid item xs={12} md={12} >
<Typography variant="h3" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "center" }}>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'pleaseLloginMessage2' }) }} />
<SafeHtml html={intl.formatMessage({ id: 'pleaseLloginMessage2' })} />
</Typography>
</Grid>
</center>


+ 4
- 2
src/pages/iAmSmart/SuccessCallback/index.js 查看文件

@@ -12,6 +12,7 @@ import { useNavigate } from "react-router-dom";
import Loadable from 'components/Loadable';
const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent')));
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import SafeHtml from 'components/SafeHtml';

import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'
const BackgroundHead = {
@@ -69,8 +70,9 @@ const Index = () => {
component="div"
variant="h3"
sx={{ mt: 4, maxWidth: 900, textAlign: 'center' }}
dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'loginSuccessMessage2' }) }}
/>
>
<SafeHtml html={intl.formatMessage({ id: 'loginSuccessMessage2' })} />
</Typography>
) : (
<Typography component="div" variant="h3" sx={{ mt: 4, maxWidth: 900, textAlign: 'center' }}>
<FormattedMessage id="iAmSmartLoginSuccessHeading" />


+ 78
- 0
src/utils/sanitizeHtml.js 查看文件

@@ -0,0 +1,78 @@
import DOMPurify from 'dompurify';

const ALLOWED_TAGS = [
'a',
'b',
'blockquote',
'br',
'caption',
'col',
'colgroup',
'div',
'em',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'hr',
'i',
'img',
'li',
'ol',
'p',
'span',
'strong',
'style',
'sub',
'sup',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'tr',
'u',
'ul'
];

const ALLOWED_ATTR = [
'href',
'title',
'target',
'rel',
'src',
'alt',
'width',
'height',
'colspan',
'rowspan',
'scope',
'class',
'style'
];

if (typeof window !== 'undefined') {
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
if (node.tagName === 'A' && node.getAttribute('target') === '_blank') {
node.setAttribute('rel', 'noopener noreferrer');
}
});
}

/**
* Strict allow-list sanitize for API / i18n HTML before innerHTML.
* Scripts, event handlers, iframes, and javascript: URLs are dropped.
*/
export function sanitizeHtml(dirty) {
if (dirty == null) {
return '';
}
return DOMPurify.sanitize(String(dirty), {
ALLOWED_TAGS,
ALLOWED_ATTR,
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i
});
}

+ 53
- 0
src/utils/sanitizeHtml.test.js 查看文件

@@ -0,0 +1,53 @@
import { sanitizeHtml } from './sanitizeHtml';

describe('sanitizeHtml', () => {
test('strips script tags and event handlers', () => {
const clean = sanitizeHtml('<p onclick="alert(1)">Hi<script>alert(1)</script></p>');
expect(clean).toContain('Hi');
expect(clean.toLowerCase()).not.toContain('script');
expect(clean.toLowerCase()).not.toContain('onclick');
expect(clean).not.toContain('alert(1)');
});

test('strips javascript URLs and iframes', () => {
const clean = sanitizeHtml(
'<a href="javascript:alert(1)">x</a><iframe src="https://evil.example"></iframe>'
);
expect(clean.toLowerCase()).not.toContain('javascript');
expect(clean.toLowerCase()).not.toContain('iframe');
expect(clean).not.toContain('evil.example');
});

test('keeps safe formatting, https links, and tables', () => {
const clean = sanitizeHtml(
'<p>Hello <strong>world</strong></p><a href="https://pnsps.gld.gov.hk/">PNSPS</a><table><tr><td>cell</td></tr></table>'
);
expect(clean).toContain('<p>');
expect(clean).toMatch(/<(strong|b)>world<\/(strong|b)>/);
expect(clean).toContain('https://pnsps.gld.gov.hk/');
expect(clean).toContain('cell');
});

test('keeps mailto links', () => {
const clean = sanitizeHtml('<a href="mailto:[email protected]">mail</a>');
expect(clean).toContain('mailto:[email protected]');
});

test('adds rel on target=_blank links', () => {
const clean = sanitizeHtml('<a href="https://egazette.gld.gov.hk/en" target="_blank">Gazette</a>');
expect(clean).toContain('target="_blank"');
expect(clean).toContain('noopener');
});

test('null and empty become empty string', () => {
expect(sanitizeHtml(null)).toBe('');
expect(sanitizeHtml(undefined)).toBe('');
expect(sanitizeHtml('')).toBe('');
});

test('img onerror is dropped', () => {
const clean = sanitizeHtml('<img src="https://example.com/a.png" onerror="alert(1)">');
expect(clean.toLowerCase()).not.toContain('onerror');
expect(clean).not.toContain('alert(1)');
});
});

+ 13
- 1
yarn.lock 查看文件

@@ -2748,7 +2748,7 @@
dependencies:
"@types/jest" "*"

"@types/trusted-types@^2.0.2":
"@types/trusted-types@^2.0.2", "@types/trusted-types@^2.0.7":
version "2.0.7"
resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz"
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
@@ -4680,6 +4680,13 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1:
dependencies:
domelementtype "^2.2.0"

dompurify@^3.4.15:
version "3.4.15"
resolved "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz"
integrity sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==
optionalDependencies:
"@types/trusted-types" "^2.0.7"

domutils@^1.7.0:
version "1.7.0"
resolved "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz"
@@ -5718,6 +5725,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==

fsevents@^2.3.2, fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==

function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"


Loading…
取消
儲存