Html-react-parser: html-react-parser๊ฐ€ XSS๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๊นŒ?

์— ๋งŒ๋“  2019๋…„ 03์›” 08์ผ  ยท  9์ฝ”๋ฉ˜ํŠธ  ยท  ์ถœ์ฒ˜: remarkablemark/html-react-parser

html-react-parser๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ CMS์—์„œ HTML์„ ์‚ญ์ œํ•˜๊ณ  ๊ตฌ๋ฌธ ๋ถ„์„ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. XSS ๊ณต๊ฒฉ์—์„œ ์ž…๋ ฅ์„ ํšจ๊ณผ์ ์œผ๋กœ ์‚ญ์ œํ•ฉ๋‹ˆ๊นŒ? https://stackoverflow.com/questions/29044518/safe-alternative-to-dangerouslysetinnerhtml#answer -48261046 ๊ทธ๋ ‡๊ฒŒ ์ฃผ์žฅํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด README์˜ ์–ด๋”˜๊ฐ€์— ์ด๊ฒƒ์„ ๋ฌธ์„œํ™”/๊ด‘๊ณ ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์„ ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ์ž‘์—…ํ•ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

question

๊ฐ€์žฅ ์œ ์šฉํ•œ ๋Œ“๊ธ€

๋Œ€์šฉ๋Ÿ‰์ด๊ธฐ ๋•Œ๋ฌธ์— Sanitize-html์„ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋Œ€์‹  dompurify๋ฅผ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ 10๋ฐฐ ๋” ์ž‘๊ณ  CSS๋ฅผ ์ œ๊ฑฐํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

import parse, { domToReact } from 'html-react-parser'
import DOMPurify from 'dompurify'
import React from 'react'

// export function replaceNode() {}

export default function html(html, opts = {}) {
  return parse(DOMPurify.sanitize(html), {
    ...{
      replace: replaceNode,
    },
    ...opts,
  })
}

html('<iframe src=javascript:alert("xss")></iframe>')

๋ชจ๋“  9 ๋Œ“๊ธ€

์ข‹์€ ์งˆ๋ฌธ @dave-stevens-net!

๋ถˆํ–‰ํžˆ๋„ ๊ทธ๋ ‡์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ ์ด์œ ๋Š” ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์—„๊ฒฉํ•˜์ง€ ์•Š๊ณ  ์œ ์—ฐํ•˜๊ฒŒ ๋งŒ๋“ค๊ธฐ๋กœ ์„ ํƒํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๊ต์ฒด ์˜ต์…˜์ด ์žˆ์ง€๋งŒ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ๊ณต๊ฒฉ์— ๋Œ€ํ•ด ๊ฒ€์‚ฌํ•˜๋Š” ๊ฒƒ์€ ๋„ˆ๋ฌด ๋งŽ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋Œ€์‹  ์œ„ํ—˜ํ•˜๊ฒŒ SetInnerHTML ๊ณผ ํ•จ๊ป˜ XSS ์ƒˆ๋‹ˆํƒ€์ด์ €๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

์•Œ์•„ ๋‘˜๋งŒ ํ•œ. ๋น ๋ฅธ ์‘๋‹ต์— ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

๋‹น์‹ ์€ ๋งค์šฐ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด @dave-stevens-net ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€์ด๋ผ๋ฉด ๋ฌธ์ œ๋ฅผ ์ข…๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ?

@dave-stevens-net ๋‚˜๋Š” ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ XSS์— ์•ˆ์ „ํ•˜์ง€ ์•Š๋‹ค๋Š” ๊ฒƒ์— ๋Œ€ํ•ด ์ผ์ฐ์ด ๋ง์„ ์ž˜๋ชปํ–ˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‚˜๋Š” ์›๋ž˜ dangerouslySetInnerHTML ์ด ์—ฌ๊ธฐ ์— ์˜์กดํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ XSS-์•ˆ์ „ํ•˜์ง€ ์•Š๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ XSS ์ทจ์•ฝ์ ์„ ์žฌํ˜„ํ•  ์ˆ˜ ์—†๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์ด ์˜ˆ์ œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋Š” ๋‚ด ๋ฐ”์ด์˜ฌ๋ฆฐ์„ ์ฐธ์กฐํ•˜์‹ญ์‹œ์˜ค.

XSS ๊ณต๊ฒฉ์„ ์žฌํ˜„ํ•˜๋Š” ๋ฐ ์šด์ด ์žˆ๋‹ค๋ฉด ์•Œ๋ ค์ฃผ์‹ญ์‹œ์˜ค.

๊ฐ„๋‹จํ•œ XSS ๊ณต๊ฒฉ์„ ์žฌํ˜„ํ•˜๋Š” ๋ฐ ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค. ๋” ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‚ด ๋ฐ”์ด์˜ฌ๋ฆฐ์„ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค.

https://www.in-secure.org/misc/xss/xss.html ์—์„œ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค.

Sanitize -html ํŒจํ‚ค์ง€ ์ข…์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ Sanitize ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์ฝ”๋”ฉํ–ˆ์Šต๋‹ˆ๋‹ค.

import React from 'react'
import sanitizeHtml from 'sanitize-html'

const Sanitize = ({ html }) => {
    const clean = sanitizeHtml(html, {
        allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'span']),
        allowedAttributes: {
           ...
        },
    })
    return (
        <span
            className="sanitized-html"
            dangerouslySetInnerHTML={{ __html: clean }}
        />
    )
}
export default Sanitize

์‚ฌ์šฉ ์˜ˆ:

<Sanitize html={data.wordpressPage.title} />

@harveydf ์ข‹์€ ๋ฐœ๊ฒฌ! ๋ฐ”์ด์˜ฌ๋ฆฐ์„ ๋งŒ๋“ค๊ณ  ๊ณต์œ ํ•ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

README.md ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ XSS์— ์•ˆ์ „ํ•˜์ง€ ์•Š๋‹ค๋Š” ์ ์— ์œ ์˜ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๋Œ€์šฉ๋Ÿ‰์ด๊ธฐ ๋•Œ๋ฌธ์— Sanitize-html์„ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋Œ€์‹  dompurify๋ฅผ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ 10๋ฐฐ ๋” ์ž‘๊ณ  CSS๋ฅผ ์ œ๊ฑฐํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

import parse, { domToReact } from 'html-react-parser'
import DOMPurify from 'dompurify'
import React from 'react'

// export function replaceNode() {}

export default function html(html, opts = {}) {
  return parse(DOMPurify.sanitize(html), {
    ...{
      replace: replaceNode,
    },
    ...opts,
  })
}

html('<iframe src=javascript:alert("xss")></iframe>')

dompurify @k1sul1์„ ์‚ฌ์šฉํ•˜์—ฌ ์ ‘๊ทผ ๋ฐฉ์‹์„ ๊ณต์œ ํ•ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

๊ท€ํ•˜์˜ ์˜ˆ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ Repl.it ๋ฐ๋ชจ๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

์ด ํŽ˜์ด์ง€๊ฐ€ ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?
0 / 5 - 0 ๋“ฑ๊ธ‰