HTML 보안 라이브러리 적용하기

Server - Node.js

먼저, 서버에서 HTML을 살균하는 것이 가장 효과적인 방법 중 하나이므로, 서버 측에서 사용자가 입력한 데이터를 안전하게 처리하기 위해 sanitize-html 라이브러리를 적용하는 방법을 알아보자. ES modules 방식으로 작성된 코드를 기반으로 한다.

이 라이브러리는 HTML 문자열에서 스크립트 태그와 이벤트 핸들러를 제거하여 사용자 입력을 안전하게 처리할 수 있도록 도와준다.


1. NPM 설치

$ npm install sanitize-html
$ npm install -D @types/sanitize-html

2. 적용

// middlewares/sanitize.ts

import sanitizeHtml from "sanitize-html";
import { Response, Request, NextFunction } from "express";

export const sanitize =
  (fields: string[]) => (req: Request, res: Response, next: NextFunction) => {
    try {
      if (!req.body) return next();
      fields.forEach((field) => {
        if (req.body[field]) {
          req.body[field] = sanitizeHtml(req.body[field], {
            allowedTags: sanitizeHtml.defaults.allowedTags.concat([
              "img",
              "h1",
              "h2",
            ]),
            allowedAttributes: {
              "*": ["style", "class"],
              a: ["href", "name", "target"],
              img: ["src", "alt"],
            },
            selfClosing: [
              "img",
              "br",
              "hr",
              "area",
              "base",
              "basefont",
              "input",
              "link",
              "meta",
            ],
            allowProtocolRelative: true,
          });
        }
      });
      next();
    } catch (error) {
      next(error);
    }
  };
// routes/post.ts

import express from "express";
import { sanitize } from "../middleware/sanitizeHTML";
import * as postController from "../controller/post.js";

const router = express.Router();

router.post("/", sanitize(["title", "content"]), postController.createPost);
router.put("/:id", sanitize(["title", "content"]), postController.updatePost);

router.post(
  "/:postId/comments",
  sanitize(["comment"]),
  postController.createComment
);

export default router;

Client - React


다음으로, 클라이언트 측에서도 사용자 입력을 안전하게 처리하기 위해 dompurify 라이브러리를 적용하는 방법을 알아보자.

아래 코드는 react-quill 텍스트 에디터 라이브러리를 사용하여 사용자가 게시물을 작성하고, 상대방에게 공유하는 코드의 일부에서 발췌한 코드이다.

이 코드에서 사용된 dangerouslySetInnerHTML 속성은 HTML 문자열을 렌더링하는 데 사용되지만, 사용자 입력을 그대로 렌더링하면 XSS(Cross-Site Scripting) 공격에 취약해질 수 있다.


import { useEffect } from "react";
// ...
import styles from "./styles.module.scss";

export default function Post({ post, refetch, refetchPost }: Props) {
  const { joinInfo } = useAuthContext();

  // ...

  return (
    <article className={styles.post}>
      // ...
      {joinInfo.role !== "teacher" ? (
        <span>
          내용: <div dangerouslySetInnerHTML={{ __html: post!.content }} />
        </span>
      ) : (
        <QuillEditor
          ref={editorRef}
          placeholder="본문을 입력해 주세요. 대용량 이미지는 하단 첨부파일을 이용해 주세요."
          value={post ? post.content : ""}
        />
      )}
    </article>
  );
}

sanitize-html 라이브러리를 사용해도 되지만, dompurify 라이브러리를 사용하는 이유는 무엇일까?


성능

DOMPurify는 클라이언트 사이드에 특화되어 있어 브라우저 환경에서 sanitize-html 보다 더 빠른 성능을 제공한다.

브라우저의 보안 모델과 호환성

브라우저의 DOM API를 직접 사용하여 작동하기 때문에, 브라우저의 내장 보안 모델과 더 잘 통합되며, 이는 XSS 취약점에 대한 보호를 강화할 수 있다.

용도와 특화

DOMPurify는 XSS 방지에 특화되어 있으며, 클라이언트 측에서 DOM을 안전하게 조작하는 데 필요한 기능만을 제공한다.

패키지 크기

웹 애플리케이션에서 로딩 시간과 성능이 중요한 요소인 경우, DOMPurifysanitize-html보다 작은 패키지 크기로 인해 선호될 수 있다.

물론 sanitize-html을 클라이언트 사이드에서 사용하는 것에 문제가 있는 것은 아니며, 서버와 클라이언트에서 일관된 살균 로직을 사용하고 싶은 경우에는 sanitize-html이 좋은 선택이 될 수 있다. 결국 선택은 사용 사례와 성능 요구사항, 개발 환경에 따라 달라질 수 있다.

따라서, 사용자 입력을 안전하게 처리하기 위해 dompurify 라이브러리를 사용하여 HTML 문자열을 정화하는 방법을 알아보자.

1. NPM 설치

$ npm i dompurify
$ npm i -D @types/dompurify

2. 적용


import { useEffect } from "react";
import DOMPurify from "dompurify";
// ...
import styles from "./styles.module.scss";

export default function Post({ post, refetch, refetchPost }: Props) {
  const { joinInfo } = useAuthContext();
  const cleanHTML = DOMPurify.sanitize(post!.content);

  // ...

  return (
    <article className={styles.post}>
      // ...
      {joinInfo.role !== "teacher" ? (
        <span>
          내용: <div dangerouslySetInnerHTML={{ __html: cleanHTML }} />
        </span>
      ) : (
        <QuillEditor
          ref={editorRef}
          placeholder="본문을 입력해 주세요. 대용량 이미지는 하단 첨부파일을 이용해 주세요."
          value={post ? cleanHTML : ""}
        />
      )}
    </article>
  );
}

마치며


Full-stack 개발자로 업무를 진행하면서, 사용해보고 싶은 기술이나 라이브러리등을 적용해볼 수 있는 기회가되어 일정 부분 만족하며 일하고 있지만, 1인 개발로 인해 많은 부분을 혼자 공부하고 적용해야 하는 점이 아쉽다.
예전에 보안 전문가가 되겠다며 다녔던 학원에서 XSS(Cross-Site Scripting) 공격에 대해 배웠던 기억이 있어서, 대충은 알고 있었지만 어떻게 방어로직을 적용할 수 있는지 몰랐다. Chat GPT가 알려준 ‘살균(sanitization) 과정을 거치지 않은 HTML은 위험하다’는 경고를 준 덕분에 HTML 살균 방법에 대해 알아보다가 적용하게 된것이다.

이번 포스팅에서는, 살균 라이브러리를 사용해야하는 이유와, 사용방법에 대해서만 알아 보았지만 다음 포스팅에서는 XSS(Cross-Site Scripting)공격은 어떻게 이뤄지는지, 살균 라이브러리를 사용하면 어떻게 막아지는지에 대해 조금 더 깊이있게 공부하는 시간을 가져야겠다.

sanitize-html Node.js React HTML보안 XSS