솔적솔적

결국 그렇게 저장했는가, 그에 따른 보안은 안전한가(브라우저 저장소 결정2편) 본문

카테고리 없음

결국 그렇게 저장했는가, 그에 따른 보안은 안전한가(브라우저 저장소 결정2편)

카드값줘체리 2024. 10. 22. 19:11

웹에서 '로그인 상태 유지' 또는 '자동 로그인' 기능을 구현할 때

어느 저장소에 무엇을 두느냐는 보안, 편의성 균형의 문제다.

 

 

 

1편에서 저장소 종류와 특성을 정리했다면

2편에서는 공격 벡터인 XXS/CSRF를 기준으로 실전 방어법과

설계 패턴을 이리저리 생각하고 구글링한 결과를 정리해볼 예정이다.

 

 

 

(*이건 그저 구현하며 저의 생각을 정리하는 내용이지 정답은 아닙니다.)

- Access Token, Refresh Token 같은 것은

가능한 클라이언트쪽에서 JS접근 불가한 HttpOnly 쿠키 + Secure + SameSite 설정으로 관리하는 것으로하는 것이 좋다생각한다.

- Access Token은 가능한 **메모리(짧은 수명)**에서 관리하고 필요 시 refresh cookie로 재발급한다.

- 비민감한 데이터, 대용량인 데이터를 저장해야할 경우는 IndexedDB가 적합하며

- 로컬스토리지는 편할 수 있지만 XXS에 취약하므로 토큰 저장으로는 사용하면 위험, 권장치않는다. 비민감 UI 상태 혹은 편의성 데이터만 사용.

 

 

 

 

위협 모델(무엇을 막아야 하나

-> xxs, csrf, 브라우저 취약점, 물리적 접근 or 공유 기기로 인한 남은 세션으로 계정 탈취 등 

 

- XXS가 뭔데? Cross Site Scripting - 공격자가 스크립트를 주입해 브라우저 저장소인 로컬, 세션, 인덱스드디비의 데이터와 DOM에 접근하여 토큰 탈취, 조작한다.

 

- CSRF가 뭔데? Cross-site Request Forgery - 사용자의 인증 쿠키가 자동으로 전송되어 의도치 않은 요청이 서버에서 실행됨.

 

 

브라우저 취약점/확장 - 악성 확장이나 브라우저 취약점으로 저장소 훔침 가능

물리적 접근/ 공유 기기 - 공용 PC에서 남은 세션 등

 

 

지금 이 위협 모델들을 기준으로 설계해보면 어느 정도 보안은 철저하게 구축되리라 믿으며 시작.

 

- Access Token은 메모리, Refresh는 HttpOnly

=> 이 방식으로 한다면 js로는 refresh token에 접근이 불가하여 XSS로 부터 안전도가 크게 올라간다.

CSRF는 SameSite, CSRF 토큰 또는 doble-submit cookie로 방어

 

그리고 Refresh token도 탈취되면 위험하니 Refresh 시 새 refresh token 발행이전 토큰 무효화를 적용시켜야한다.

 

토큰과 세션관리할 때 되도록이면 토큰 수명은 너무 길지 않게 설정하고

로그아웃 시 서버에서 토큰 무효화, 클라이언트 저장소에서 초기화해줘야한다.

 

이것 말고도 보안 관련 ESLint 규칙, 정적 분석 도구를 도입하는 방법

- eslint-plugin-no-unsanitized (Mozilla) — innerHTML, insertAdjacentHTML 같은 직접 DOM 삽입을 금지하거나 허용된 sanitizer 사용을 강제. XSS 예방에 유용.

 

- eslint-plugin-xss — XSS 관련 사용 패턴(예: 위험한 문자열 조작)을 찾아주는 추가 규칙

- @typescript-eslint + eslint:recommended + eslint-plugin-react 등 기본 룰셋(타입스크립트/React 프로젝트용)

- 규칙 추가

금지: eval, new Function(), document.write, innerHTML

* 참고: ESLint 보안 플러그인은 정적 검사라서 완벽치 않고, 결과는 반드시 리뷰해야 함 npm공식 문서 참고

 

 

.eslintrc.js 예시

module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended'
  ],
  plugins: ['no-unsanitized','xss','security'],
  rules: {
    'no-unsanitized/method': 'error',
    'no-unsanitized/property': 'error',
    'security/detect-eval-with-expression': 'error',
    'xss/no-mixed-html': 'warn'
  }
};

 

 

 

 

운영 모니터링: Sentry 설정, 민감정보 스크럽

운영모니터링으로 Sentry로 수집하여 에러 알림을 통해 로깅 수집이 가능하다.

sentry에는 에러/예외 + 퍼포먼스 트레이싱 + 리치 컨텍스트 제공

 

beforeSend 훅을 사용해 이벤트가 전송되기 전에 민감정보 제거/마스킹.

import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [new BrowserTracing()],
  tracesSampleRate: 0.1,
  environment: process.env.NODE_ENV,
  release: process.env.SENTRY_RELEASE,
  beforeSend(event, hint) {
    // 요청 헤더의 인증 정보 제거
    if (event.request?.headers) {
      delete event.request.headers['authorization'];
      delete event.request.headers['cookie'];
    }
    // event.user 객체에 민감 정보가 있다면 지우기
    if (event.user) {
      delete event.user.email;
      delete event.user.ip_address;
    }
    // 추가: breadcrumbs/extra 에 들어간 민감 데이터 스크럽
    if (event.breadcrumbs) {
      event.breadcrumbs = event.breadcrumbs.map(b => {
        if (b.data && b.data.requestBody) {
          // 예시 마스킹
          b.data.requestBody = '[REDACTED]';
        }
        return b;
      });
    }
    return event;
  }
});
  • Alert rules: 신규 에러, 급증 에러 알림 설정(Slack/PagerDuty)
  • 샘플링/레이트리밋: 비용관리 및 과다수집 방지

 

 

정리

  • Refresh Token: HttpOnly + Secure + SameSite 설정
  • Access Token: 메모리(짧은 수명)로 관리
  • local/session storage: 토큰 절대 저장 금지
  • IndexedDB: 비민감 대용량 데이터용
  • CSRF 방어: SameSite + CSRF 토큰 적용
  • 토큰 회전 및 서버 무효화(로그아웃) 구현
  • ESLint 보안 플러그인 도입 + Husky + lint-staged 적용
  • Sentry: beforeSend로 PII 제거, sourcemap 업로드, 알림 룰 설정

 

 

 

 

참고자료