feat(sanitize-html): integrate sanitize-html for product descriptions

- Add sanitize-html package to sanitize product descriptions, ensuring safe rendering of HTML content.
- Update PrerenderProduct and ProductDetailPage components to utilize sanitize-html for improved security and content integrity.
- Enhance error handling in ProductDetailPage to fallback to plain text if HTML parsing fails.
This commit is contained in:
sebseb7
2025-11-13 06:44:06 +01:00
parent 2bb9a151a3
commit 9e9d9ada4a
4 changed files with 94 additions and 7 deletions

73
package-lock.json generated
View File

@@ -27,6 +27,7 @@
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^15.6.0", "react-i18next": "^15.6.0",
"react-router-dom": "^7.6.2", "react-router-dom": "^7.6.2",
"sanitize-html": "^2.17.0",
"sharp": "^0.34.2", "sharp": "^0.34.2",
"socket.io-client": "^4.7.5" "socket.io-client": "^4.7.5"
}, },
@@ -5265,6 +5266,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/default-browser": { "node_modules/default-browser": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
@@ -8886,7 +8896,6 @@
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -9458,6 +9467,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/parse-srcset": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
"license": "MIT"
},
"node_modules/parse5": { "node_modules/parse5": {
"version": "7.3.0", "version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -9666,7 +9681,6 @@
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -10555,6 +10569,60 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/sanitize-html": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz",
"integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==",
"license": "MIT",
"dependencies": {
"deepmerge": "^4.2.2",
"escape-string-regexp": "^4.0.0",
"htmlparser2": "^8.0.0",
"is-plain-object": "^5.0.0",
"parse-srcset": "^1.0.2",
"postcss": "^8.3.11"
}
},
"node_modules/sanitize-html/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/sanitize-html/node_modules/htmlparser2": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1",
"entities": "^4.4.0"
}
},
"node_modules/sanitize-html/node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/saxes": { "node_modules/saxes": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
@@ -11227,7 +11295,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"

View File

@@ -45,6 +45,7 @@
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^15.6.0", "react-i18next": "^15.6.0",
"react-router-dom": "^7.6.2", "react-router-dom": "^7.6.2",
"sanitize-html": "^2.17.0",
"sharp": "^0.34.2", "sharp": "^0.34.2",
"socket.io-client": "^4.7.5" "socket.io-client": "^4.7.5"
}, },

View File

@@ -9,6 +9,7 @@ import {
Toolbar, Toolbar,
Button Button
} from '@mui/material'; } from '@mui/material';
import sanitizeHtml from 'sanitize-html';
import Footer from './components/Footer.js'; import Footer from './components/Footer.js';
import { Logo } from './components/header/index.js'; import { Logo } from './components/header/index.js';
import ProductImage from './components/ProductImage.js'; import ProductImage from './components/ProductImage.js';
@@ -539,7 +540,17 @@ class PrerenderProduct extends React.Component {
React.createElement( React.createElement(
'div', 'div',
{ {
dangerouslySetInnerHTML: { __html: product.description }, dangerouslySetInnerHTML: {
__html: sanitizeHtml(product.description, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
allowedAttributes: {
'*': ['class', 'style'],
'a': ['href', 'title'],
'img': ['src', 'alt', 'width', 'height']
},
disallowedTagsMode: 'discard'
})
},
style: { style: {
fontFamily: '"Roboto","Helvetica","Arial",sans-serif', fontFamily: '"Roboto","Helvetica","Arial",sans-serif',
fontSize: '1rem', fontSize: '1rem',

View File

@@ -9,6 +9,7 @@ import LinkIcon from "@mui/icons-material/Link";
import CodeIcon from "@mui/icons-material/Code"; import CodeIcon from "@mui/icons-material/Code";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import parse from "html-react-parser"; import parse from "html-react-parser";
import sanitizeHtml from "sanitize-html";
import AddToCartButton from "./AddToCartButton.js"; import AddToCartButton from "./AddToCartButton.js";
import ProductImage from "./ProductImage.js"; import ProductImage from "./ProductImage.js";
import Product from "./Product.js"; import Product from "./Product.js";
@@ -1314,9 +1315,16 @@ class ProductDetailPage extends Component {
"& strong": { fontWeight: 600 }, "& strong": { fontWeight: 600 },
}} }}
> >
{product.description ? ( {product.description ? (() => {
parse(product.description) try {
) : upgrading ? ( // Sanitize HTML to remove invalid tags
return parse(sanitizeHtml(product.description));
} catch (error) {
console.warn('Failed to parse product description HTML:', error);
// Fallback to rendering as plain text if HTML parsing fails
return <span>{product.description}</span>;
}
})() : upgrading ? (
<Box sx={{ textAlign: "center", py: 2 }}> <Box sx={{ textAlign: "center", py: 2 }}>
<Typography variant="body1" color="text.secondary"> <Typography variant="body1" color="text.secondary">
{this.props.t ? this.props.t('product.loadingDescription') : 'Produktbeschreibung wird geladen...'} {this.props.t ? this.props.t('product.loadingDescription') : 'Produktbeschreibung wird geladen...'}