mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-08 06:41:45 -05:00
chore: added DOMPurify to prevent xss (#1894)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
aa63c89a6a
commit
64db29417d
@@ -1,97 +0,0 @@
|
||||
/*!
|
||||
* Sanitize an HTML string
|
||||
* (c) 2021 Chris Ferdinandi, MIT License, https://gomakethings.com
|
||||
* @param {String} str The HTML string to sanitize
|
||||
* @return {String} The sanitized string
|
||||
*/
|
||||
export function cleanHtml(str: string): string {
|
||||
/**
|
||||
* Convert the string to an HTML document
|
||||
* @return {Node} An HTML document
|
||||
*/
|
||||
function stringToHTML() {
|
||||
let parser = new DOMParser();
|
||||
let doc = parser.parseFromString(str, "text/html");
|
||||
return doc.body || document.createElement("body");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove <script> elements
|
||||
* @param {Node} html The HTML
|
||||
*/
|
||||
function removeScripts(html: Element) {
|
||||
let scripts = html.querySelectorAll("script");
|
||||
scripts.forEach((script) => {
|
||||
script.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the attribute is potentially dangerous
|
||||
* @param {String} name The attribute name
|
||||
* @param {String} value The attribute value
|
||||
* @return {Boolean} If true, the attribute is potentially dangerous
|
||||
*/
|
||||
/**
|
||||
* Check if the attribute is potentially dangerous
|
||||
*/
|
||||
function isPossiblyDangerous(name: string, value: string): boolean {
|
||||
let val = value.replace(/\s+/g, "").toLowerCase();
|
||||
if (
|
||||
["src", "href", "xlink:href", "srcdoc"].includes(name) &&
|
||||
(val.includes("javascript:") || val.includes("data:") || val.includes("<script>"))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (name.startsWith("on")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove potentially dangerous attributes from an element
|
||||
* @param {Node} elem The element
|
||||
*/
|
||||
function removeAttributes(elem: Element) {
|
||||
// Loop through each attribute
|
||||
// If it's dangerous, remove it
|
||||
let atts = elem.attributes;
|
||||
for (let i = atts.length - 1; i >= 0; i--) {
|
||||
let { name, value } = atts[i];
|
||||
if (isPossiblyDangerous(name, value)) {
|
||||
elem.removeAttribute(name);
|
||||
} else if (name === "srcdoc") {
|
||||
// Recursively sanitize srcdoc content
|
||||
elem.setAttribute(name, cleanHtml(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove dangerous stuff from the HTML document's nodes
|
||||
* @param {Node} html The HTML document
|
||||
*/
|
||||
/**
|
||||
* Clean the HTML nodes recursively
|
||||
* @param {Element} html The HTML element
|
||||
*/
|
||||
function clean(html: Element) {
|
||||
let nodes = Array.from(html.children);
|
||||
for (let node of nodes) {
|
||||
removeAttributes(node);
|
||||
clean(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the string to HTML
|
||||
let html = stringToHTML();
|
||||
|
||||
// Sanitize it
|
||||
removeScripts(html);
|
||||
clean(html);
|
||||
|
||||
// If the user wants HTML nodes back, return them
|
||||
// Otherwise, pass a sanitized string back
|
||||
return html.innerHTML;
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"downlevelIteration": true,
|
||||
},
|
||||
"downlevelIteration": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,23 +33,24 @@
|
||||
"clean": "rimraf .turbo node_modules dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/embed-snippet": "1.1.2",
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@preact/preset-vite": "^2.8.1",
|
||||
"isomorphic-dompurify": "^2.2.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"concurrently": "8.2.2",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-turbo": "latest",
|
||||
"postcss": "^8.4.33",
|
||||
"preact": "^10.19.3",
|
||||
"react-date-picker": "^10.6.0",
|
||||
"serve": "14.2.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"terser": "^5.26.0",
|
||||
"vite": "^5.0.12",
|
||||
"vite-plugin-dts": "^3.7.0",
|
||||
"vite-tsconfig-paths": "^4.2.3",
|
||||
"serve": "14.2.1",
|
||||
"concurrently": "8.2.2",
|
||||
"@calcom/embed-snippet": "1.1.2"
|
||||
"vite-tsconfig-paths": "^4.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
import { cleanHtml } from "@/lib/cleanHtml";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface HtmlBodyProps {
|
||||
htmlString: string | undefined;
|
||||
questionId: string;
|
||||
}
|
||||
|
||||
export default function HtmlBody({ htmlString, questionId }: HtmlBodyProps) {
|
||||
const [safeHtml, setSafeHtml] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (htmlString) {
|
||||
import("isomorphic-dompurify").then((DOMPurify) => {
|
||||
setSafeHtml(DOMPurify.sanitize(htmlString));
|
||||
});
|
||||
}
|
||||
}, [htmlString]);
|
||||
|
||||
export default function HtmlBody({ htmlString, questionId }: { htmlString?: string; questionId: string }) {
|
||||
if (!htmlString) return null;
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor={questionId}
|
||||
className="fb-htmlbody" // styles are in global.css
|
||||
dangerouslySetInnerHTML={{ __html: cleanHtml(htmlString) }}></label>
|
||||
dangerouslySetInnerHTML={{ __html: safeHtml }}></label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/*!
|
||||
* Sanitize an HTML string
|
||||
* (c) 2021 Chris Ferdinandi, MIT License, https://gomakethings.com
|
||||
* @param {String} str The HTML string to sanitize
|
||||
* @return {String} The sanitized string
|
||||
*/
|
||||
export function cleanHtml(str: string): string {
|
||||
/**
|
||||
* Convert the string to an HTML document
|
||||
* @return {Node} An HTML document
|
||||
*/
|
||||
function stringToHTML() {
|
||||
let parser = new DOMParser();
|
||||
let doc = parser.parseFromString(str, "text/html");
|
||||
return doc.body || document.createElement("body");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove <script> elements
|
||||
* @param {Node} html The HTML
|
||||
*/
|
||||
function removeScripts(html: Element) {
|
||||
let scripts = html.querySelectorAll("script");
|
||||
scripts.forEach((script) => {
|
||||
script.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the attribute is potentially dangerous
|
||||
* @param {String} name The attribute name
|
||||
* @param {String} value The attribute value
|
||||
* @return {Boolean} If true, the attribute is potentially dangerous
|
||||
*/
|
||||
/**
|
||||
* Check if the attribute is potentially dangerous
|
||||
*/
|
||||
function isPossiblyDangerous(name: string, value: string): boolean {
|
||||
let val = value.replace(/\s+/g, "").toLowerCase();
|
||||
if (
|
||||
["src", "href", "xlink:href", "srcdoc"].includes(name) &&
|
||||
(val.includes("javascript:") || val.includes("data:") || val.includes("<script>"))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (name.startsWith("on")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove potentially dangerous attributes from an element
|
||||
* @param {Node} elem The element
|
||||
*/
|
||||
function removeAttributes(elem: Element) {
|
||||
// Loop through each attribute
|
||||
// If it's dangerous, remove it
|
||||
let atts = elem.attributes;
|
||||
for (let i = atts.length - 1; i >= 0; i--) {
|
||||
let { name, value } = atts[i];
|
||||
if (isPossiblyDangerous(name, value)) {
|
||||
elem.removeAttribute(name);
|
||||
} else if (name === "srcdoc") {
|
||||
// Recursively sanitize srcdoc content
|
||||
elem.setAttribute(name, cleanHtml(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove dangerous stuff from the HTML document's nodes
|
||||
* @param {Node} html The HTML document
|
||||
*/
|
||||
/**
|
||||
* Clean the HTML nodes recursively
|
||||
* @param {Element} html The HTML element
|
||||
*/
|
||||
function clean(html: Element) {
|
||||
let nodes = Array.from(html.children);
|
||||
for (let node of nodes) {
|
||||
removeAttributes(node);
|
||||
clean(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the string to HTML
|
||||
let html = stringToHTML();
|
||||
|
||||
// Sanitize it
|
||||
removeScripts(html);
|
||||
clean(html);
|
||||
|
||||
// If the user wants HTML nodes back, return them
|
||||
// Otherwise, pass a sanitized string back
|
||||
return html.innerHTML;
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
"include": ["."],
|
||||
"exclude": ["build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2021.String"],
|
||||
},
|
||||
"lib": ["ES2021.String"]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user