Log에서 password 필터링하기

00_8_3·2022년 8월 21일
0

SEEME 이슈

목록 보기
10/10

왜 필터링을 해야하는가?

서버를 구축하게 되면 로그를 관리하게 됩니다.
사용자가 만들어내는 로그 중에는 credential한 정보도 있기 때문에 필터링을 해야합니다. (DB에는 사용자의 Request를 넣어주면서 Log만 필터링)

기존 express 서버의 req.body의 filtering 하던 방식을 리팩토링 해보록 하겠습니다.

기존 코드

기존코드의 경우 문제점

  • passwordpasswordconfirm이라는 두 단어만 필터링.(하드코딩)
  • depth 1까지만 필터링.
const loggingReqMiddleware(req: Requset, res: Response){
	const body = req.body;
  
  const filteredBody = { ...body };
    Object.keys(body).forEach((k) => {
      if (k.toLocaleLowerCase().indexOf("password") > -1) {
        filteredBody.password = "FILTERED";
      }

      if (k.toLocaleLowerCase().indexOf("passwordconfirm") > -1) {
        filteredBody.passwordConfirm = "FILTERED";
      }
    });

    logger.info(`Parameters : ${JSON.stringify(filteredBody)}`);
}

수정 1

기존 하드코딩 되어있떤 필터링할 단어를 배열로 선언하여 관리.
하지만 여전히 문제점이 있습니다.

  • depth 1까지만 필터링
  • body의 크기가 길어질 수록 쓸모없는 처리량이 늘어남.

    CENSOR_WORD_LIST가 모두 필터링 되었다고 해도 반복문 진행됨.

const CENSOR_WORD_LIST = ["password", "passwordConfirm"] as const;
const lowerCaseCensorWordList = CENSOR_WORD_LIST.map((list) =>
  list.toLowerCase()
);

function xorArrFromBrr(arr: string[], brr: string[]) {
  return arr
  // include 시간복잡도 O(n)
  // 검열할 단어 개수 : N
  // 전체 키 수 : M
  // O(n) * N
    .map((key) => key.toLowerCase())
    .filter((key) => !brr.includes(key));
}

const loggingReqMiddleware(req: Requset, res: Response){
	const body = req.body;
  
  logger.info(
      `Parameters : ${JSON.stringify(
        xorArrFromBrr(Object.keys({ ...body }), lowerCaseCensorWordList)
      )}`
    );
  

    logger.info(`Parameters : ${JSON.stringify(filteredBody)}`);
}

수정 2

  • Nested depth에도 필터링을 지원하기 위해
    Body의 key가 string이 아닌 Object인 경우 재귀를 한다.

  • 기존 Body의 key 배열 전체 수 만큼 접근하여 필터링을 한 반면
    필터링할 문자 배열 CENSOR_WORD_LIST 수 만큼만 반복을 돌며
    find를 사용하여 첫 요소를 만나는 경우 반환을 하여 불필요한 검색을 막음.

type BodyWithIndexSignature = Record<string, string | unknown>;

function filteringNestedBody(
  body: BodyWithIndexSignature,
  censoredWords: string[]
) {
  const keys = Object.keys(body);

  const keysOfObjectInBody = keys.filter((key) => body[key] instanceof Object);
  if (keysOfObjectInBody.length > 0) {
    for (const key of keysOfObjectInBody) {
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      filteringNestedBody(body[key] as BodyWithIndexSignature, censoredWords);
    }
  }

    // find 시간복잡도 O(1) 최악 O(N)
    // 검열할 단어 개수 : N
    // 전체 키 수 : M
    // O(n) * N
  for (const word of censoredWords) {
    const key = keys.find((v) => v.toLocaleLowerCase() === word);
    if (key) {
      // 값을 변경해서 보여주거나, key자체를 삭제.
      body[key] = "FILTERED";
      // delete body[key];
    }
  }
  return body;
}

const loggingReqMiddleware({body}: Requset, res: Response){
	//const body = req.body;
  
  // DB에 저장되는 data들이 필터링이 되면 안되게 깊은복사를 해줍니다.
  logger.info(
      `Parameters : ${JSON.stringify(
        filteringNestedBody({ ...body }, lowerCaseCensorWordList)
      )}`
    );
  

    logger.info(`Parameters : ${JSON.stringify(filteredBody)}`);
}

시간복잡도

includes와 find 모두 최악의 경우 O(N)의 시간 복잡도를 갖음.

  • 수정 1의 경우 Body의 모든 key를 반복
  • 수정 2의 경우 검열할 문자열 개수 반복

보통 Body의 키 수가 검열할 문자열 개수보다 많기 때문에
수정 2의 방법을 사용하는걸로 합니다.

결말

실제로는 검열할 단어와 Body key가 엄청나게 많지 않을 것이고
수정1에 비해 수정2의 경우 재귀를 고려했기 때문에 수정2가 드라마틱한 성능 향상을 가진다고 할 수 없겠지만

효율을 생각해보며 연습겸 리팩토링하고 작성해봅니다.

0개의 댓글