-
Notifications
You must be signed in to change notification settings - Fork 4
Add a new package: safe-html-react-parser #199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
137b61e
65a2c81
12dd1ce
2950b18
1b2085b
80a2486
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "@naverpay/safe-html-react-parser": major | ||
| --- | ||
|
|
||
| Add a new package: safe-html-react-parser | ||
|
|
||
| PR: [Add a new package: safe-html-react-parser](https://github.com/NaverPayDev/pie/pull/199) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| # safe-html-react-parser | ||
|
|
||
| A secure wrapper for **html-react-parser** with **isomorphic-dompurify** that automatically sanitizes HTML before parsing. | ||
|
|
||
| ## What it does | ||
|
|
||
| - 🛡️ **Security**: Automatically sanitizes malicious HTML using DOMPurify | ||
| - ⚛️ **React**: Seamlessly integrates with html-react-parser | ||
| - 🌐 **Universal**: Works in both browser and Node.js (SSR) environments | ||
| - 🏷️ **Custom Tags**: Handles project-specific tags like `<custom>` safely | ||
|
|
||
| ## Requirements | ||
|
|
||
| - Node.js >=20.19.5: isomorphic-dompurify@^2.30.1 | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| npm install @naverpay/safe-html-react-parser | ||
| ``` | ||
|
|
||
| ## Basic Usage | ||
|
|
||
| ```tsx | ||
| import { safeParse } from '@naverpay/safe-html-react-parser' | ||
|
|
||
| // Basic usage - automatically sanitizes dangerous HTML | ||
| const Component = () => { | ||
| const maliciousHtml = '<p>Hello <script>alert("XSS")</script>World</p>' | ||
| return <div>{safeParse(maliciousHtml)}</div> | ||
| } | ||
| // Result: <div><p>Hello World</p></div> | ||
| ``` | ||
|
|
||
| ## API | ||
|
|
||
| ### `safeParse(htmlString, options?)` | ||
|
|
||
| Parses HTML string into React elements with automatic XSS protection. | ||
|
|
||
| #### Parameters | ||
|
|
||
| - `htmlString` (string): The HTML string to parse | ||
| - `options` (SafeParseOptions, optional): Configuration options | ||
|
|
||
| #### Options | ||
|
|
||
| ```typescript | ||
| interface SafeParseOptions extends HTMLReactParserOptions { | ||
| // DOMPurify configuration | ||
| sanitizeConfig?: DOMPurify.Config | ||
|
|
||
| // Custom tags to preserve during sanitization | ||
| preserveCustomTags?: string[] | ||
| } | ||
| ``` | ||
|
|
||
| #### Returns | ||
|
|
||
| React elements or array of React elements | ||
|
|
||
| ## Advanced Usage | ||
|
|
||
| ### Custom Sanitization Config | ||
|
|
||
| ```tsx | ||
| import { safeParse } from '@naverpay/safe-html-react-parser' | ||
|
|
||
| const html = '<div class="content"><style>body{color:red}</style><p>Text</p></div>' | ||
|
|
||
| const result = safeParse(html, { | ||
| sanitizeConfig: { | ||
| ALLOWED_TAGS: ['div', 'p', 'style'], // Allow style tags | ||
| ALLOWED_ATTR: ['class'], | ||
| ALLOW_ARIA_ATTR: true | ||
| } | ||
| }) | ||
| ``` | ||
|
|
||
| ### Preserving Custom Tags | ||
|
|
||
| Use `preserveCustomTags` to preserve project-specific tags that would otherwise be removed: | ||
|
|
||
| ```tsx | ||
| import { safeParse } from '@naverpay/safe-html-react-parser' | ||
|
|
||
| // Preserve custom tags like <g>, <path>, etc. | ||
| const svgContent = '<g><path d="M10,10 L20,20"/></g>' | ||
|
|
||
| const result = safeParse(svgContent, { | ||
| preserveCustomTags: ['g', 'path'], | ||
| replace: (domNode) => { | ||
| if (domNode.name === 'g') { | ||
| return <g {...domNode.attribs}>{/* custom rendering */}</g> | ||
| } | ||
| if (domNode.name === 'path') { | ||
| return <path {...domNode.attribs} /> | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
|
|
||
| ### Using with html-react-parser Options | ||
|
|
||
| All html-react-parser options are supported: | ||
|
|
||
| ```tsx | ||
| import { safeParse } from '@naverpay/safe-html-react-parser' | ||
|
|
||
| const html = '<div id="content"><p>Hello</p><img src="image.jpg" alt="test"/></div>' | ||
|
|
||
| const result = safeParse(html, { | ||
| replace: (domNode) => { | ||
| if (domNode.name === 'img') { | ||
| return <img {...domNode.attribs} loading="lazy" /> | ||
| } | ||
| }, | ||
| trim: true | ||
| }) | ||
| ``` | ||
|
|
||
| ## Default Allowed Tags | ||
|
|
||
| By default, the following HTML tags are allowed: | ||
|
|
||
| ```typescript | ||
| ALLOWED_TAGS: [ | ||
| 'p', 'br', 'strong', 'em', 'b', 'i', 'u', 'span', 'div', | ||
| 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h', | ||
| 'ul', 'ol', 'li', 'dl', 'dt', 'dd', | ||
| 'a', 'img' | ||
| ] | ||
| ``` | ||
|
|
||
| ## Security Notes | ||
|
|
||
| - All HTML is sanitized by DOMPurify before parsing | ||
| - Dangerous tags like `<script>`, `<iframe>`, `<object>` are automatically removed | ||
| - Event handlers like `onclick`, `onload` are stripped out | ||
| - Only safe attributes are preserved by default | ||
|
|
||
| ## Built with | ||
|
|
||
| - [html-react-parser@^5.2.7](https://github.com/remarkablemark/html-react-parser) - HTML string to React element parser | ||
| - [isomorphic-dompurify@^2.30.1](https://github.com/kkomelin/isomorphic-dompurify) - Universal XSS sanitizer | ||
|
|
||
| ## License | ||
|
|
||
| MIT |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| { | ||
| "name": "@naverpay/safe-html-react-parser", | ||
| "version": "0.0.0", | ||
| "description": "A secure wrapper for html-react-parser with isomorphic-dompurify that automatically sanitizes HTML before parsing.", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/NaverPayDev/pie/tree/main/packages/safe-html-react-parser" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/NaverPayDev/pie/issues" | ||
| }, | ||
| "keywords": [ | ||
| "naver", | ||
| "naverpay", | ||
| "react", | ||
| "html-parser", | ||
| "dompurify", | ||
| "safe-html" | ||
| ], | ||
| "author": "@NaverPayDev/frontend", | ||
| "dependencies": { | ||
| "html-react-parser": "^5.2.7", | ||
| "isomorphic-dompurify": "^2.30.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/react": "0.14 || 15 || 16 || 17 || 18 || 19", | ||
| "react": "0.14 || 15 || 16 || 17 || 18 || 19" | ||
| }, | ||
| "peerDependencies": { | ||
| "@types/react": "0.14 || 15 || 16 || 17 || 18 || 19", | ||
| "react": "0.14 || 15 || 16 || 17 || 18 || 19" | ||
| }, | ||
| "scripts": { | ||
| "clean": "rm -rf dist", | ||
| "build": "npm run clean && vite build" | ||
| }, | ||
| "main": "./dist/cjs/index.js", | ||
| "module": "./dist/esm/index.mjs", | ||
| "types": "./dist/cjs/index.d.ts", | ||
| "exports": { | ||
| ".": { | ||
| "import": { | ||
| "types": "./dist/esm/index.d.mts", | ||
| "default": "./dist/esm/index.mjs" | ||
| }, | ||
| "require": { | ||
| "types": "./dist/cjs/index.d.ts", | ||
| "default": "./dist/cjs/index.js" | ||
| } | ||
| }, | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "sideEffects": false, | ||
| "homepage": "https://naverpaydev.github.io/pie/docs/docs/@naverpay/safe-html-react-parser/" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| /** | ||
| * Utilizes html-react-parser with DOMPurify for safe HTML parsing | ||
| */ | ||
| import * as htmlReactParser from 'html-react-parser' | ||
| import DOMPurify from 'isomorphic-dompurify' | ||
|
|
||
| import type {DOMNode, HTMLReactParserOptions} from 'html-react-parser' | ||
|
|
||
| // html-react-parser가 esm에서 cjs 모듈을 re-export 하는 문제 처리 | ||
| // In CJS: htmlReactParser.default.default is the actual function | ||
| // In ESM: htmlReactParser.default is the function | ||
| const parse = ((htmlReactParser as any).default?.default || | ||
| (htmlReactParser as any).default || | ||
| htmlReactParser) as typeof htmlReactParser.default | ||
|
|
||
| export interface SafeParseOptions extends HTMLReactParserOptions { | ||
| /** | ||
| * DOMPurify Options | ||
| */ | ||
| sanitizeConfig?: DOMPurify.Config | ||
| /** | ||
| * Custom tag preservation option (temporary conversion before and after DOMPurify processing) | ||
| */ | ||
| preserveCustomTags?: string[] | ||
| } | ||
|
|
||
| export const DEFAULT_SANITIZE_CONFIG: DOMPurify.Config = { | ||
| ALLOWED_TAGS: [ | ||
| 'p', | ||
| 'br', | ||
| 'strong', | ||
| 'em', | ||
| 'b', | ||
| 'i', | ||
| 'u', | ||
| 'span', | ||
| 'div', | ||
| 'h1', | ||
| 'h2', | ||
| 'h3', | ||
| 'h4', | ||
| 'h5', | ||
| 'h6', | ||
| 'h', | ||
| 'ul', | ||
| 'ol', | ||
| 'li', | ||
| 'dl', | ||
| 'dt', | ||
| 'dd', | ||
| 'a', | ||
| 'img', | ||
| ], | ||
| KEEP_CONTENT: true, | ||
| } | ||
|
|
||
| /** | ||
| * @param htmlString - HTML string to parse | ||
| * @param options - html-react-parser options with DOMPurify settings | ||
| * @returns Parsed React elements | ||
| */ | ||
| export function safeParse(htmlString: string, options: SafeParseOptions = {}) { | ||
| const {sanitizeConfig = DEFAULT_SANITIZE_CONFIG, preserveCustomTags, ...parserOptions} = options | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I customize ALLOWED_TAGS externally, it doesn’t merge with the defaults — it completely overrides them, right?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought merging might be better, but it makes sense to keep it this way since someone might want to remove certain tags. The default config is exported anyway, so developers can merge it however they want.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, so I exported the |
||
|
|
||
| // Temporarily convert custom tags to safe tags to preserve them during DOMPurify processing | ||
| const processedHtml = | ||
| preserveCustomTags?.reduce( | ||
| (str, tag) => | ||
| str | ||
| .replace(new RegExp(`<${tag}>`, 'g'), `<span data-custom-tag="${tag}">`) | ||
| .replace(new RegExp(`</${tag}>`, 'g'), '</span>'), | ||
| htmlString, | ||
| ) || htmlString | ||
|
|
||
| const sanitizedHtml = DOMPurify.sanitize(processedHtml, sanitizeConfig) | ||
|
|
||
| return parse(sanitizedHtml, { | ||
| ...parserOptions, | ||
| replace: (domNode, index) => { | ||
| if ( | ||
| domNode.type === 'tag' && | ||
| domNode.name === 'span' && | ||
| domNode.attribs && | ||
| domNode.attribs['data-custom-tag'] | ||
| ) { | ||
| const customTagNode = { | ||
| ...domNode, | ||
| name: domNode.attribs['data-custom-tag'], | ||
| attribs: domNode.attribs, | ||
| } as DOMNode | ||
|
|
||
| if (parserOptions.replace) { | ||
| const userResult = parserOptions.replace(customTagNode, index) | ||
| if (userResult) { | ||
| return userResult | ||
| } | ||
| } | ||
|
|
||
| return domNode | ||
| } | ||
|
|
||
| if (parserOptions.replace) { | ||
| return parserOptions.replace(domNode, index) | ||
| } | ||
| }, | ||
| }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "extends": "../../tsconfig.base.json", | ||
| "compilerOptions": { | ||
| "baseUrl": ".", | ||
| "outDir": "./dist/cjs", | ||
| "rootDir": "./src", | ||
| "emitDeclarationOnly": true, | ||
| "noUnusedLocals": false | ||
| }, | ||
| "include": ["./src", "./typings"], | ||
| "exclude": ["node_modules", "dist"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import {createViteConfig} from '@naverpay/pite' | ||
|
|
||
| export default createViteConfig({ | ||
| cwd: '.', | ||
| entry: ['./src/index.ts'], | ||
| options: { | ||
| minify: false, | ||
| }, | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I followed the peerDeps of html-react-parser.
https://github.com/remarkablemark/html-react-parser