Skip to content

Commit ab910bc

Browse files
authored
Merge pull request #199 from NaverPayDev/feature/198
Add a new package: safe-html-react-parser
2 parents cc1602b + 80a2486 commit ab910bc

7 files changed

Lines changed: 723 additions & 375 deletions

File tree

.changeset/funny-dancers-battle.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@naverpay/safe-html-react-parser": major
3+
---
4+
5+
Add a new package: safe-html-react-parser
6+
7+
PR: [Add a new package: safe-html-react-parser](https://github.com/NaverPayDev/pie/pull/199)
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# safe-html-react-parser
2+
3+
A secure wrapper for **html-react-parser** with **isomorphic-dompurify** that automatically sanitizes HTML before parsing.
4+
5+
## What it does
6+
7+
- 🛡️ **Security**: Automatically sanitizes malicious HTML using DOMPurify
8+
- ⚛️ **React**: Seamlessly integrates with html-react-parser
9+
- 🌐 **Universal**: Works in both browser and Node.js (SSR) environments
10+
- 🏷️ **Custom Tags**: Handles project-specific tags like `<custom>` safely
11+
12+
## Requirements
13+
14+
- Node.js >=20.19.5: isomorphic-dompurify@^2.30.1
15+
16+
## Installation
17+
18+
```bash
19+
npm install @naverpay/safe-html-react-parser
20+
```
21+
22+
## Basic Usage
23+
24+
```tsx
25+
import { safeParse } from '@naverpay/safe-html-react-parser'
26+
27+
// Basic usage - automatically sanitizes dangerous HTML
28+
const Component = () => {
29+
const maliciousHtml = '<p>Hello <script>alert("XSS")</script>World</p>'
30+
return <div>{safeParse(maliciousHtml)}</div>
31+
}
32+
// Result: <div><p>Hello World</p></div>
33+
```
34+
35+
## API
36+
37+
### `safeParse(htmlString, options?)`
38+
39+
Parses HTML string into React elements with automatic XSS protection.
40+
41+
#### Parameters
42+
43+
- `htmlString` (string): The HTML string to parse
44+
- `options` (SafeParseOptions, optional): Configuration options
45+
46+
#### Options
47+
48+
```typescript
49+
interface SafeParseOptions extends HTMLReactParserOptions {
50+
// DOMPurify configuration
51+
sanitizeConfig?: DOMPurify.Config
52+
53+
// Custom tags to preserve during sanitization
54+
preserveCustomTags?: string[]
55+
}
56+
```
57+
58+
#### Returns
59+
60+
React elements or array of React elements
61+
62+
## Advanced Usage
63+
64+
### Custom Sanitization Config
65+
66+
```tsx
67+
import { safeParse } from '@naverpay/safe-html-react-parser'
68+
69+
const html = '<div class="content"><style>body{color:red}</style><p>Text</p></div>'
70+
71+
const result = safeParse(html, {
72+
sanitizeConfig: {
73+
ALLOWED_TAGS: ['div', 'p', 'style'], // Allow style tags
74+
ALLOWED_ATTR: ['class'],
75+
ALLOW_ARIA_ATTR: true
76+
}
77+
})
78+
```
79+
80+
### Preserving Custom Tags
81+
82+
Use `preserveCustomTags` to preserve project-specific tags that would otherwise be removed:
83+
84+
```tsx
85+
import { safeParse } from '@naverpay/safe-html-react-parser'
86+
87+
// Preserve custom tags like <g>, <path>, etc.
88+
const svgContent = '<g><path d="M10,10 L20,20"/></g>'
89+
90+
const result = safeParse(svgContent, {
91+
preserveCustomTags: ['g', 'path'],
92+
replace: (domNode) => {
93+
if (domNode.name === 'g') {
94+
return <g {...domNode.attribs}>{/* custom rendering */}</g>
95+
}
96+
if (domNode.name === 'path') {
97+
return <path {...domNode.attribs} />
98+
}
99+
}
100+
})
101+
```
102+
103+
### Using with html-react-parser Options
104+
105+
All html-react-parser options are supported:
106+
107+
```tsx
108+
import { safeParse } from '@naverpay/safe-html-react-parser'
109+
110+
const html = '<div id="content"><p>Hello</p><img src="image.jpg" alt="test"/></div>'
111+
112+
const result = safeParse(html, {
113+
replace: (domNode) => {
114+
if (domNode.name === 'img') {
115+
return <img {...domNode.attribs} loading="lazy" />
116+
}
117+
},
118+
trim: true
119+
})
120+
```
121+
122+
## Default Allowed Tags
123+
124+
By default, the following HTML tags are allowed:
125+
126+
```typescript
127+
ALLOWED_TAGS: [
128+
'p', 'br', 'strong', 'em', 'b', 'i', 'u', 'span', 'div',
129+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h',
130+
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
131+
'a', 'img'
132+
]
133+
```
134+
135+
## Security Notes
136+
137+
- All HTML is sanitized by DOMPurify before parsing
138+
- Dangerous tags like `<script>`, `<iframe>`, `<object>` are automatically removed
139+
- Event handlers like `onclick`, `onload` are stripped out
140+
- Only safe attributes are preserved by default
141+
142+
## Built with
143+
144+
- [html-react-parser@^5.2.7](https://github.com/remarkablemark/html-react-parser) - HTML string to React element parser
145+
- [isomorphic-dompurify@^2.30.1](https://github.com/kkomelin/isomorphic-dompurify) - Universal XSS sanitizer
146+
147+
## License
148+
149+
MIT
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"name": "@naverpay/safe-html-react-parser",
3+
"version": "0.0.0",
4+
"description": "A secure wrapper for html-react-parser with isomorphic-dompurify that automatically sanitizes HTML before parsing.",
5+
"repository": {
6+
"type": "git",
7+
"url": "https://github.com/NaverPayDev/pie/tree/main/packages/safe-html-react-parser"
8+
},
9+
"bugs": {
10+
"url": "https://github.com/NaverPayDev/pie/issues"
11+
},
12+
"keywords": [
13+
"naver",
14+
"naverpay",
15+
"react",
16+
"html-parser",
17+
"dompurify",
18+
"safe-html"
19+
],
20+
"author": "@NaverPayDev/frontend",
21+
"dependencies": {
22+
"html-react-parser": "^5.2.7",
23+
"isomorphic-dompurify": "^2.30.1"
24+
},
25+
"devDependencies": {
26+
"@types/react": "0.14 || 15 || 16 || 17 || 18 || 19",
27+
"react": "0.14 || 15 || 16 || 17 || 18 || 19"
28+
},
29+
"peerDependencies": {
30+
"@types/react": "0.14 || 15 || 16 || 17 || 18 || 19",
31+
"react": "0.14 || 15 || 16 || 17 || 18 || 19"
32+
},
33+
"scripts": {
34+
"clean": "rm -rf dist",
35+
"build": "npm run clean && vite build"
36+
},
37+
"main": "./dist/cjs/index.js",
38+
"module": "./dist/esm/index.mjs",
39+
"types": "./dist/cjs/index.d.ts",
40+
"exports": {
41+
".": {
42+
"import": {
43+
"types": "./dist/esm/index.d.mts",
44+
"default": "./dist/esm/index.mjs"
45+
},
46+
"require": {
47+
"types": "./dist/cjs/index.d.ts",
48+
"default": "./dist/cjs/index.js"
49+
}
50+
},
51+
"./package.json": "./package.json"
52+
},
53+
"files": [
54+
"dist"
55+
],
56+
"sideEffects": false,
57+
"homepage": "https://naverpaydev.github.io/pie/docs/docs/@naverpay/safe-html-react-parser/"
58+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
/**
3+
* Utilizes html-react-parser with DOMPurify for safe HTML parsing
4+
*/
5+
import * as htmlReactParser from 'html-react-parser'
6+
import DOMPurify from 'isomorphic-dompurify'
7+
8+
import type {DOMNode, HTMLReactParserOptions} from 'html-react-parser'
9+
10+
// html-react-parser가 esm에서 cjs 모듈을 re-export 하는 문제 처리
11+
// In CJS: htmlReactParser.default.default is the actual function
12+
// In ESM: htmlReactParser.default is the function
13+
const parse = ((htmlReactParser as any).default?.default ||
14+
(htmlReactParser as any).default ||
15+
htmlReactParser) as typeof htmlReactParser.default
16+
17+
export interface SafeParseOptions extends HTMLReactParserOptions {
18+
/**
19+
* DOMPurify Options
20+
*/
21+
sanitizeConfig?: DOMPurify.Config
22+
/**
23+
* Custom tag preservation option (temporary conversion before and after DOMPurify processing)
24+
*/
25+
preserveCustomTags?: string[]
26+
}
27+
28+
export const DEFAULT_SANITIZE_CONFIG: DOMPurify.Config = {
29+
ALLOWED_TAGS: [
30+
'p',
31+
'br',
32+
'strong',
33+
'em',
34+
'b',
35+
'i',
36+
'u',
37+
'span',
38+
'div',
39+
'h1',
40+
'h2',
41+
'h3',
42+
'h4',
43+
'h5',
44+
'h6',
45+
'h',
46+
'ul',
47+
'ol',
48+
'li',
49+
'dl',
50+
'dt',
51+
'dd',
52+
'a',
53+
'img',
54+
],
55+
KEEP_CONTENT: true,
56+
}
57+
58+
/**
59+
* @param htmlString - HTML string to parse
60+
* @param options - html-react-parser options with DOMPurify settings
61+
* @returns Parsed React elements
62+
*/
63+
export function safeParse(htmlString: string, options: SafeParseOptions = {}) {
64+
const {sanitizeConfig = DEFAULT_SANITIZE_CONFIG, preserveCustomTags, ...parserOptions} = options
65+
66+
// Temporarily convert custom tags to safe tags to preserve them during DOMPurify processing
67+
const processedHtml =
68+
preserveCustomTags?.reduce(
69+
(str, tag) =>
70+
str
71+
.replace(new RegExp(`<${tag}>`, 'g'), `<span data-custom-tag="${tag}">`)
72+
.replace(new RegExp(`</${tag}>`, 'g'), '</span>'),
73+
htmlString,
74+
) || htmlString
75+
76+
const sanitizedHtml = DOMPurify.sanitize(processedHtml, sanitizeConfig)
77+
78+
return parse(sanitizedHtml, {
79+
...parserOptions,
80+
replace: (domNode, index) => {
81+
if (
82+
domNode.type === 'tag' &&
83+
domNode.name === 'span' &&
84+
domNode.attribs &&
85+
domNode.attribs['data-custom-tag']
86+
) {
87+
const customTagNode = {
88+
...domNode,
89+
name: domNode.attribs['data-custom-tag'],
90+
attribs: domNode.attribs,
91+
} as DOMNode
92+
93+
if (parserOptions.replace) {
94+
const userResult = parserOptions.replace(customTagNode, index)
95+
if (userResult) {
96+
return userResult
97+
}
98+
}
99+
100+
return domNode
101+
}
102+
103+
if (parserOptions.replace) {
104+
return parserOptions.replace(domNode, index)
105+
}
106+
},
107+
})
108+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"compilerOptions": {
4+
"baseUrl": ".",
5+
"outDir": "./dist/cjs",
6+
"rootDir": "./src",
7+
"emitDeclarationOnly": true,
8+
"noUnusedLocals": false
9+
},
10+
"include": ["./src", "./typings"],
11+
"exclude": ["node_modules", "dist"]
12+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {createViteConfig} from '@naverpay/pite'
2+
3+
export default createViteConfig({
4+
cwd: '.',
5+
entry: ['./src/index.ts'],
6+
options: {
7+
minify: false,
8+
},
9+
})

0 commit comments

Comments
 (0)