Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/funny-dancers-battle.md
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)
149 changes: 149 additions & 0 deletions packages/safe-html-react-parser/README.md
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
58 changes: 58 additions & 0 deletions packages/safe-html-react-parser/package.json
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"
},
Comment on lines +25 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

Copy link
Contributor Author

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

"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/"
}
108 changes: 108 additions & 0 deletions packages/safe-html-react-parser/src/index.ts
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, so I exported the DEFAULT_SANITIZE_CONFIG . You can use it when you need an extension of the default value.


// 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)
}
},
})
}
12 changes: 12 additions & 0 deletions packages/safe-html-react-parser/tsconfig.json
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"]
}
9 changes: 9 additions & 0 deletions packages/safe-html-react-parser/vite.config.mjs
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,
},
})
Loading