HTML 보안 라이브러리 적용하기
Server - Node.js
먼저, 서버에서 HTML을 살균하는 것이 가장 효과적인 방법 중 하나이므로, 서버 측에서 사용자가 입력한 데이터를 안전하게 처리하기 위해 sanitize-html
라이브러리를 적용하는 방법을 알아보자. ES modules
방식으로 작성된 코드를 기반으로 한다.
이 라이브러리는 HTML 문자열에서 스크립트 태그와 이벤트 핸들러를 제거하여 사용자 입력을 안전하게 처리할 수 있도록 도와준다.
1. NPM 설치
$ npm install sanitize-html
$ npm install -D @types/sanitize-html
2. 적용
- 미들웨어 함수 생성 후 라우터에 적용 시킨다.
- 미들웨어 함수는 요청(req)과 응답(res) 객체에 접근할 수 있으며, 요청을 처리한 후 다음 미들웨어 함수로 제어를 전달하는 next 함수를 호출해야 한다.
// 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);
}
};
- 메서드 체이닝을 통해 미들웨어 함수를 적용할 때 인자로 필드명을 배열로 전달한다.
- 필드명은 요청 본문(req.body)에서 해당 필드의 값을 가져와 처리하는 데 사용된다.
- 이렇게 하면 요청 본문에서 해당 필드의 값이 sanitizeHtml 함수를 통해 안전한 HTML이 사용되어 XSS 공격을 방지할 수 있다.
// 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을 안전하게 조작하는 데 필요한 기능만을 제공한다.
패키지 크기
웹 애플리케이션에서 로딩 시간과 성능이 중요한 요소인 경우, DOMPurify
는 sanitize-html
보다 작은 패키지 크기로 인해 선호될 수 있다.
물론 sanitize-html을 클라이언트 사이드에서 사용하는 것에 문제가 있는 것은 아니며, 서버와 클라이언트에서 일관된 살균 로직을 사용하고 싶은 경우에는 sanitize-html이 좋은 선택이 될 수 있다. 결국 선택은 사용 사례와 성능 요구사항, 개발 환경에 따라 달라질 수 있다.
따라서, 사용자 입력을 안전하게 처리하기 위해 dompurify
라이브러리를 사용하여 HTML 문자열을 정화하는 방법을 알아보자.
1. NPM 설치
$ npm i dompurify
$ npm i -D @types/dompurify
2. 적용
DOMPurify.sanitize()
메서드를 사용하여 사용자 입력을 정화한다. 이 메서드는 입력된 HTML 문자열을 안전한 HTML로 변환하여 반환한다.
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)
공격은 어떻게 이뤄지는지, 살균 라이브러리를 사용하면 어떻게 막아지는지에 대해 조금 더 깊이있게 공부하는 시간을 가져야겠다.