diff --git a/package-lock.json b/package-lock.json index 1208cb0..a4f5eae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "react-dom": "^19.1.0", "react-i18next": "^15.6.0", "react-router-dom": "^7.6.2", + "sanitize-html": "^2.17.0", "sharp": "^0.34.2", "socket.io-client": "^4.7.5" }, @@ -5265,6 +5266,15 @@ "dev": true, "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": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", @@ -8886,7 +8896,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -9458,6 +9467,12 @@ "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": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -9666,7 +9681,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -10555,6 +10569,60 @@ "dev": true, "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": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -11227,7 +11295,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" diff --git a/package.json b/package.json index c540d5f..e3a79b2 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "react-dom": "^19.1.0", "react-i18next": "^15.6.0", "react-router-dom": "^7.6.2", + "sanitize-html": "^2.17.0", "sharp": "^0.34.2", "socket.io-client": "^4.7.5" }, diff --git a/src/PrerenderProduct.js b/src/PrerenderProduct.js index 4221a26..11f7b28 100644 --- a/src/PrerenderProduct.js +++ b/src/PrerenderProduct.js @@ -9,6 +9,7 @@ import { Toolbar, Button } from '@mui/material'; +import sanitizeHtml from 'sanitize-html'; import Footer from './components/Footer.js'; import { Logo } from './components/header/index.js'; import ProductImage from './components/ProductImage.js'; @@ -539,7 +540,17 @@ class PrerenderProduct extends React.Component { React.createElement( '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: { fontFamily: '"Roboto","Helvetica","Arial",sans-serif', fontSize: '1rem', diff --git a/src/components/ProductDetailPage.js b/src/components/ProductDetailPage.js index db5e154..be3362a 100644 --- a/src/components/ProductDetailPage.js +++ b/src/components/ProductDetailPage.js @@ -9,6 +9,7 @@ import LinkIcon from "@mui/icons-material/Link"; import CodeIcon from "@mui/icons-material/Code"; import { Link } from "react-router-dom"; import parse from "html-react-parser"; +import sanitizeHtml from "sanitize-html"; import AddToCartButton from "./AddToCartButton.js"; import ProductImage from "./ProductImage.js"; import Product from "./Product.js"; @@ -1314,9 +1315,16 @@ class ProductDetailPage extends Component { "& strong": { fontWeight: 600 }, }} > - {product.description ? ( - parse(product.description) - ) : upgrading ? ( + {product.description ? (() => { + try { + // 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 {product.description}; + } + })() : upgrading ? ( {this.props.t ? this.props.t('product.loadingDescription') : 'Produktbeschreibung wird geladen...'}