관리 메뉴

거니의 velog

(17) 프론트엔드 프로젝트 : 포스트 조회 기능 구현하기 4 본문

React_프론트엔드 프로젝트

(17) 프론트엔드 프로젝트 : 포스트 조회 기능 구현하기 4

Unlimited00 2024. 2. 23. 19:25

(3) HTML 필터링하기

* sanitize-html 이라는 라이브러리를 사용하여 HTML을 필터링해 보겠다. 이 라이브러리는 HTML을 작성하고 보여 주어야 하는 서비스에서 매우 유용하다. 단순히 HTML을 제거하는 기능뿐만 아니라 특정 HTML만 허용하는 기능도 있기 때문에 글쓰기 API에서 사용하면 손쉽게 악성 스크립트 삽입을 막을 수 있다.

* 백엔드 프로젝트 디렉터리에서 yarn을 사용하여 sanitize-html을 설치하자.

$ yarn add sanitize-html

* 이어서 백엔드 프로젝트의 post.ctrl.js를 수정한다. 먼저 맨 위에 sanitize-html을 불러 오자.

[src/api/posts/posts.ctrl.js]

import Post from '../../models/post';
import mongoose from 'mongoose';
import Joi from 'joi';
import sanitizeHtml from 'sanitize-html';

(...)

* 앞으로 총 세 개의 함수를 수정할 것이다. 우선 포스트 목록을 조회하는 list 함수를 다음과 같이 수정해 보자.

[src/api/posts/posts.ctrl.js]

// html을 없애고 내용이 너무 길면 200자로 제한하는 함수
const removeHtmlAndShorten = (body) => {
  const filtered = sanitizeHtml(body, {
    allowedTags: [],
  });
  return filtered.length < 200 ? filtered : `${filtered.slice(0, 200)}...`;
};

/*
  GET /api/posts?username=&tag=&page=
*/
export const list = async (ctx) => {
  (...)

  try {
    const posts = await Post.find(query)
      .sort({ _id: -1 })
      .limit(10)
      .skip((page - 1) * 10)
      .lean()
      .exec();
    const postCount = await Post.countDocuments(query).exec();
    ctx.set('Last-Page', Math.ceil(postCount / 10));
    ctx.body = posts.map((post) => ({
      ...post,
      body: removeHtmlAndShorten(post.body),
    }));
  } catch (e) {
    ctx.throw(500, e);
  }
};

* 기존에는 문자열 길이만 제한했는데, 이번에는 HTML을 제거하고 문자열 길이를 200자로 제한했다. 이 작업을 위해 removeHtmlAndShorten 이라는 함수도 새로 만들었다.

* 그 다음에 수정해야 할 API는 포스트의 작성 및 수정에 관한 것이다. 포스트를 작성하고 수정할 때는 모든 HTML을 제거하는 것이 아니라, 악성 스크립트가 주입되는 것을 방지하기 위해 특정 태그들만 허용해 준다.

* sanitize-html은 HTML의 특정 태그와 특정 속성만 허용할 수 있다. 코드의 상단에 sanitizeOptions 라는 객체를 선언하자.

[src/api/posts/posts.ctrl.js]

import Post from '../../models/post';
import mongoose from 'mongoose';
import Joi from 'joi';
import sanitizeHtml from 'sanitize-html';

const { ObjectId } = mongoose.Types;

const sanitizeOption = {
  allowedTags: [
    'h1',
    'h2',
    'b',
    'i',
    'u',
    's',
    'p',
    'ul',
    'ol',
    'li',
    'blockquote',
    'a',
    'img',
  ],
  allowedAttributes: {
    a: ['href', 'name', 'target'],
    img: ['src'],
    li: ['class'],
  },
  allowedSchemes: ['data', 'http'],
};

(...)

* sanitizeOptions 객체는 HTML을 필터링할 때 허용할 것을 설정해 준다. 더 자세한 설정은 다음의 공식 메뉴얼을 참고하자.

https://www.npmjs.com/package/sanitize-html

 

sanitize-html

Clean up user-submitted HTML, preserving allowlisted elements and allowlisted attributes on a per-element basis. Latest version: 2.12.1, last published: 18 hours ago. Start using sanitize-html in your project by running `npm i sanitize-html`. There are 175

www.npmjs.com

* 이제 write 함수와 update 함수를 업데이트해 보자.

[src/api/posts/posts.ctrl.js]

/*
  POST /api/posts
  {
    title: '제목',
    body: '내용',
    tags: ['태그1', '태그2']
  }
*/
export const write = async (ctx) => {
  (...)
  
  const post = new Post({
    title,
    body: sanitizeHtml(body, sanitizeOption),
    tags,
    user: ctx.state.user,
  });
  try {
    await post.save();
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};

* 이어서 update 함수도 수정해 보자.

[src/api/posts/posts.ctrl.js]

/*
  PATCH /api/posts/:id
  {
    title: '수정',
    body: '수정 내용',
    tags: ['수정', '태그']
  }
*/
export const update = async (ctx) => {
  (...)

  const nextData = { ...ctx.request.body }; // 객체를 복사하고
  // body 값이 주어졌으면 HTML 필터링
  if (nextData.body) {
    nextData.body = sanitizeHtml(nextData.body);
  }

  try {
    const post = await Post.findByIdAndUpdate(id, nextData, {
      new: true, // 이 값을 설정하면 업데이트된 데이터를 반환합니다.
      // false 일 때에는 업데이트 되기 전의 데이터를 반환합니다.
    }).exec();
    if (!post) {
      ctx.status = 404;
      return;
    }
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};

* 코드를 모두 수정했으면, http://localhost:3000/ 에 들어가서 HTML 태그가 제거된 상태로 포스트 목록이 나타나는지 확인해 보자.

HTML 필터링

* 이전에는 p 태그와 b 태그가 있었는데, 더 이상 보이지 않는다.

* 그리고 다음과 같이 Postman으로 script 태그를 넣어서 포스트 작성 API를 요청해 보자.

POST http://localhost:4000/api/posts

{
    "title" : "스크립트 넣어 보기",
    "body" : "<p>안녕하세요 <b>리액트</b> <script>alert('hello world!')</script>",
    "tags" : ["스크립트"]
}

* body 안에 p 태그, b 태그, script 태그를 사용했다. 이렇게 요청했을 때 script 태그는 제외되고 나머지 태그만 남아 있다면 HTML 필터링은 성공이다. 다음 JSON은 예시 응답이다.

{
    "tags": [
        "스크립트"
    ],
    "_id": "65d879aa5363631c74a7ad3e",
    "title": "스크립트 넣어 보기",
    "body": "<p>안녕하세요 <b>리액트</b> </p>",
    "user": {
        "_id": "65d5b4de491f6907e4e96f6e",
        "username": "bbbb1234"
    },
    "publishedDate": "2024-02-23T10:55:38.190Z",
    "__v": 0
}