Notice
Recent Posts
Recent Comments
Link
«   2026/07   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

mimizae 님의 블로그

프론트엔드의 보안 본문

FE

프론트엔드의 보안

mimizae 2026. 3. 1. 15:05

37기 DIVE SOPT에서 진행한 프로젝트 CareNA를 본격적으로 시작하기 전, 보안에 대해 공부한 과정을 담은 글입니다! 🙌🏻


 

우리 서비스 CareNA는 사용자의 건강검진 데이터 해설을 통해 본인의 건강에 대해 쉽게 이해할 수 있도록 도와주며 실제 건강관리를 위한 운동과 식단같은 행동 정보도 쉽게 탐색할 수 있는 건강관리 서비스이다.

 

 


즉!!! 민감정보(건강 검진 데이터)를 다뤄야 한다...

아직 와이어 프레임 단계이지만!!!!! 기본적으로 건강 검진 데이터 입력을 받아야 하고, 건강 검진 데이터 분석 결과를 렌더링해야 한다.

 

즉, 민감 정보 입력 민감 정보 렌더링 두 측면에서 보안을 신경 써야 한다.

프론트엔드 보안으로는 완전한 방어선을 구축할 수는 없지만… 최소한으로는 챙겨야 할 요소들에는 무엇이 있을까?


0. XSS, CSRF 방어

😈 XSS란?

공격자가 악성 스크립트를 신뢰되는 웹 사이트의 응답에 섞어 보내서 사용자의 브라우저가 그 악성 스크립트를 실행하게 만드는 공격이다.

 

흐름으로 정리해 보자면

  1. 사용자가 어떤 URL을 클릭하거나 사이트에 접속을 한다.
  2. 브라우저가 그 사이트에 요청을 보낸다.
  3. 서버가 응답을 보내는데, 해당 사이트가 XSS에 취약한 웹 사이트라서 그 응답 안에 악성 스크립트가 섞여 있다면?
  4. 브라우저는 이를 인지하지 못하고 정상 페이지의 일부로만 인식하고 사이트를 실행하며 그 악성 스크립트 자체를 함께 실행하게 된다.

왜 인지하지 못하냐면, 브라우저 입장에서는 웹 사이트의 응답 출처가 신뢰된 도메인에서 온 것이기 때문. 의심할 여지가 없다…

 

😈 XSS 공격의 종류

  • Stored XSS → XSS에 사용되는 악성 스크립트가 서버에 저장되고 서버의 응답에 담겨오는 XSS

공격자가 게시글 / 댓글 / 프로필 / OCR 텍스트 등 입력 가능한 곳에 악성 스크립트를 섞어서 저장할 때, 해당 웹 페이지의 ****서버가 그걸 검증 없이 저장하게 될 때 발생한다.

 

이제 피해자가 그 게시글 페이지에 접속하게 되면 서버가 저장된 내용을 HTML 응답에 그대로 포함하게 되고, 브라우저는 해당 악성 스크립트를 실행 시킨다.

 

(1) 공격자
    게시글/댓글/프로필/OCR 이미지 업로드
    → <script>...</script> 포함

(2) 서버
    입력 검증 없이 DB에 저장

(3) 피해자
    해당 페이지 방문

(4) 서버
    DB 내용 → HTML 응답에 포함

(5) 브라우저
    script 실행

 

  • Reflected XSS → XSS에 사용되는 악성 스크립트가 URL에 삽입되고 서버의 응답에 담겨오는 XSS

악성 스크립트가 사용자의 요청(Request)에 포함되고, 서버가 그 값을 응답(Response)에 그대로 반사(reflect)해서 브라우저가 실행하는 공격!!

 

경우 1)

검색 결과나 에러 메세지 등 웹 페이지를 이용하면서 생성되는 입력 값들에 악성 스크립트를 포함하여 서버에게 보내서 서버가 브라우저에게 응답할 때 파라미터에 삽입된 악성 스크립트를 사용자에게 그대로 전달(반사!)하면서 발생한다.

 

1. 사용자가 사이트에 접속
2. 검색창에 입력 (또는 자동 완성 / 추천 / 복붙)
   → <script>...</script> 가 포함됨
3. 브라우저가 서버에 요청 전송
4. 서버가 검색어를 그대로 HTML 응답에 포함
5. 브라우저가 script 실행

 

 

경우 2)

즉, 공격자가 악성 스크립트 문자열이 포함된 URL을 만들어 사용자가 이를 클릭하도록 유도하면, 사용자의 브라우저가 해당 요청을 서버에 보내고 서버가 그 값을 응답에 반사(reflect)하면서 XSS가 발생한다. → 이상한 메일이나 메세지의 url을 함부로 클릭하면 안 되는 이유…!!!

(1) 공격자
    https://site.com/search?q=<script>...</script> -> 링크 생성

(2) 피해자 -> 링크 클릭
    
(3) 서버 -> 요청으로 받은 q 값을 그대로 HTML 응답에 포함

(4) 브라우저 -> script 실행

 

  • DOM-based XSS → 서버 응답은 정상인데, 클라이언트가 DOM을 조작하는 과정에서 악성 스크립트가 실행 됨.

서버는 문제가 없고 프론트엔드 코드의 문제라고 생각하면 됨…!!

 

<div id="output"></div>

<script>
  const data = location.hash.substring(1);
  document.getElementById("output").innerHTML = data;
</script>

 

공격자가 만드는 URL → https://site.com/#<script>alert(1)</script>

 

사용자가 악성 스크립트 파라미터로 이루어진 URL 접속하면 서버는 완전 정상 HTML 반환하지만 브라우저가 JS을 실행할 때 location.hash 을 읽고 innerHTML에 그대로 삽입해 <script> 실행 시킨다.

 

 

😈 XSS 방지하기

 

XSS는 구조적으로 완벽하게 차단하기 어려운 취약점에 가깝다.

 

사용자 입력이 서버를 거쳐 다시 브라우저에서 실행되는 구조 자체가 문제의 근원이기 때문이다.

그래서 보통 XSS 대응은 한 가지 방법이 아니라, 여러 단계에서의 방어를 조합하는 방식으로 접근한다.

 

크게 보면 반드시 지켜야 할 원칙은 두 가지다.

입력 시에는 형식 검증 (validation)
출력 시에는 컨텍스트 기반 escape (encoding)

 

  • 입력 시: 형식 검증 (validation)

사용자가 입력하는 모든 값은 신뢰할 수 없다는 전제를 깔고 시작해야 한다.

다만, 입력 단계에서 XSS를 완전히 제거하려고 시도하는 것은 현실적으로 어렵다…!!!!

 

<script> 같은 특정 문자열을 차단하는 방식우회 방법이 너무 많고, HTML 태그, 이벤트 핸들러, 인코딩된 스크립트 등 다양한 변형을 모두 고려하기도 어렵다.

 

그래서 입력 검증은 악성 스크립트 제거보다는 형식 검증에 초점을 둔다.

 

예를 들어,

  • 숫자만 들어와야 하는 값에 문자가 섞여 있는지
  • 이메일, 아이디처럼 허용 가능한 패턴을 벗어났는지
  • 길이가 비정상적으로 긴 값은 아닌지

와 같은 검증을 통해 비정상적인 입력을 1차적으로 걸러내는 역할을 한다.

 

즉, 입력 검증은 XSS 방지의 보조 수단이지, 결정적인 해결책은 아니다.

 

  • 출력 시: 컨텍스트 기반 escape (encoding)

XSS 방지에서 가장 중요한 단계는 출력 시점이다.

서버에 저장된 데이터가 안전하다고 가정해서는 안 되고, 브라우저에 전달되는 순간 반드시 escape 되어야 한다.

 

특히 중요한 점은, 데이터가 출력되는 위치(컨텍스트)에 따라 escape 방식이 달라진다는 것이다.

  • HTML 본문에 출력될 때
  • HTML 속성 값으로 들어갈 때
  • JavaScript 코드 안에 포함될 때
  • URL 파라미터로 사용될 때

각각 요구되는 인코딩 방식이 다르다.

그래서 단순히 한 번 escape 해두는 방식이 아니라, 어디에 출력되는지를 기준으로 처리해야 한다.

 

대부분의 서버 사이드 템플릿 엔진이나 프론트엔드 프레임워크는 HTML 출력에 대해 자동 escape 기능을 제공한다. 이 기능을 끄지 않고 사용하는 것만으로도 많은 XSS를 예방할 수 있다.

 

반대로, innerHTML처럼 HTML을 직접 삽입하는 방식은 XSS로 이어질 가능성이 매우 높기 때문에 사용에 각별한 주의가 필요하드아!!!

 

😈 CSRF란?

CSRF(Cross-Site Request Forgery)는 사용자가 의도하지 않은 요청을, 사용자의 권한으로 서버에 보내게 만드는 공격이다…!!!

 

사용자가 인증된 상태에서 악의적인 사이트를 방문했을 때, 사용자의 의지와는 상관없이 공격자가 의도한 요청을 해당 웹 애플리케이션에 보내게 만드는 공격 기법!

 

CSRF 공격 흐름

  1. 사용자가 A 사이트에 로그인한다. → (쿠키 / 세션이 브라우저에 저장됨)
  2. 사용자는 로그인 상태를 유지한 채 다른 사이트(B 사이트)에 접속한다.
  3. B 사이트에는 A 사이트로 요청을 보내는 악성 코드가 숨어 있다.
  4. 사용자의 브라우저가 그 요청을 자동으로 A 사이트에 전송한다.
  5. 브라우저는 쿠키를 아무 생각 없이 함께 보낸다. 악성 코드에 의해 요청이 보내지지만 이 요청을 보내게 되는 주체는 인증된 브라우저니까!
  6. 서버는 로그인된 사용자의 요청이라고 판단해 해당 요청을 처리한다…

만 약 에… 사용자가 은행 웹사이트에 로그인한 상태에서 악성 웹사이트를 방문하게 된 경우…

공격자는 사용자의 은행 계좌에서 돈을 이체하는 요청을 자동으로 생성할 수 있다…!!!!!!!!

 

 

😈 CSRF 방지하기

 

1) CSRF Token 사용 → 가장 일반적인 방법이다!!

동작 원리!!

  • 서버가 예측 불가능한 랜덤 토큰을 발급
  • 이 토큰은 정상 페이지에서만 얻을 수 있음
  • 요청 시 토큰이 없거나 틀리면 요청 거부

CSRF Token 동작 흐름!!

  1. 서버가 랜덤한 토큰을 생성한다.
  2. 이 토큰을 HTML 폼이나 헤더에 포함한다.
  3. 요청이 들어오면 서버는 토큰을 검증한다.

공격자는 이 토큰 값을 알 수 없기 때문에, 요청을 위조할 수 없다.

 

 

2) SameSite Cookie 설정쿠키 자체에 제약을 거는 방식이다!!

Set-Cookie: session=abcd; SameSite=Lax; Secure; HttpOnly

 

Strict / Lax 옵션을 사용하면 다른 사이트에서 오는 요청에는 쿠키가 자동으로 포함되지 않는다.

 

옵션 설명
Strict 완전 차단 (다른 사이트 요청에 쿠키 안 감)
Lax 대부분의 CSRF 차단 (GET 이동 정도만 허용)
None 크로스 사이트 허용 (위험, 반드시 Secure 필요)

 

 

😈 XSS와 CSRF, 그리고 왜 XSS를 더 먼저 챙겨야 하는지

CSRF와 XSS는 둘 다 중요하지만, 실무에서 보안 우선순위를 정할 때는 항상 XSS가 앞에 온다.

이건 이론이 아니라 공격이 실제로 가능한 범위 때문에 그렇다.

 

1. XSS는 공격자가 우리 서비스 안에 들어온 상태다

 

CSRF는 어디까지나 외부 사이트에서 우리 서버로 요청을 보내는 공격이다.

그래서 공격자는 요청을 보낼 수는 있어도 서버의 응답을 볼 수 없고 화면을 조작할 수 없고 사용자의 행동을 실시간으로 통제할 수 없다

 

반대로 XSS는 다르다…

XSS가 발생하면 공격자는 우리 서비스 도메인에서 JavaScript를 실행하는 권한을 얻는다.

 

이 순간부터 공격자는 사용자가 누르는 버튼을 감시하고 사용자가 입력하는 값을 읽고 서버 API를 자유롭게 호출하고 응답 데이터까지 전부 확인한다

즉, 요청 위조가 아니라 권한 탈취 상태다.

 

2. XSS 하나면 CSRF 방어는 무력화된다

 

CSRF 방어는 공격자는 정상 페이지를 볼 수 없다는 전제를 깔고 있다. 그래서 CSRF 토큰을 숨겨두는 것이다.

하지만 XSS가 있으면 이 전제가 깨진다. 공격자는 정상 페이지를 보고 있고 DOM에 접근할 수 있고 토큰이 어디 있는지 다 안다…

 

결과적으로

  • CSRF Token → 그대로 읽어서 사용
  • SameSite Cookie → 동일 사이트 요청이라 의미 없음
  • Origin 검사 → 정상 origin

☠️☠️☠️☠️

 

즉, XSS는 CSRF 방어를 전부 우회하는 상위 공격이다.

 

3. CSRF는 설정으로 막히는 경우가 많다 (위험하긴 하지만!!)

현대 웹 환경에서는

  • SameSite=Lax 기본 적용
  • JWT + Authorization Header
  • CORS 정책 강화

이런 이유로 의도하지 않아도 CSRF가 막혀 있는 경우가 많다.


1. OCR을 통해 건강검진 통보서 스캔 (텍스트 보안, 스크립트 삽입 방지)

📷 OCR 기술이란?

OCR(Optical Character Recognition, 광학 문자 인식)은 이미지 안에 있는 글자를 사람이 아니라 컴퓨터가 읽을 수 있는 텍스트로 변환하는 기술이다.

 

조금 더 풀어서 말하면 스캔한 문서, 사진, 캡처 이미지처럼 픽셀로 이루어진 이미지를 입력으로 받아서 그 안에 포함된 문자(숫자·한글·영문 등)를 인식해 문자열 데이터로 바꾸는 과정이 OCR이다.

 

이미지에서 텍스트를 추출해 내는 OCR 기술을 구현할 때는 Tesseract.js 라이브러리나 Google Vision API를 사용한다.

둘 중에는 Google Vision API가 성능이 더 좋은 것 같다!

 

우리 서비스는 민감 정보인 건강 검진 정보를 사용자의 수기로 입력하게 할 수도, OCR 기술을 통해서 이미지를 스캔해 텍스트를 추출해 낼 수도 있다.

 

여기에서 포인트는, 이미지 보안이라기 보다는 이미지에서 추출해 낸 텍스트에 대한 보안을 챙겨야 한다.

OCR 결과는 사용자가 직접 입력한 것도 아니고 외부 라이브러리/모델이 만든 결과이기 때문에 OCR 결과 텍스트신뢰할 수 없는 사용자 입력으로 다뤄야 한다!!

 

만약… OCR로 추출한 텍스트를 검증 없이 그대로 저장하고, 이를 다시 HTML 응답에 포함해 렌더링(출력)한다면… Stored XSS의 입력 경로가 될 수 있다…!!!!

OCR은 입력 수단이 이미지일 뿐, 보안 관점에서는 일반 사용자 입력과 동일하게 검증/escape 해야 한다.

 

📷 프론트엔드 렌더링 시 주의할 점

OCR 결과 텍스트는 사용자 편의를 위해 페이지 하단의 input 필드나 화면에 자동으로 렌더링된다.

이때 단순히 DOM에 텍스트로 노출되는 것 자체는 문제가 아닐 수 있지만… HTML로 해석될 여지가 있는 방식의 렌더링은 위험하다.

 

특히 다음과 같은 방식은 피해야 한다.

  • dangerouslySetInnerHTML 사용
  • OCR 결과를 그대로 HTML로 렌더링
  • innerHTML 기반 DOM 조작

OCR 결과가 <script> 태그나 이벤트 핸들러 형태로 해석될 수 있는 구조라면, 브라우저는 이를 정상적인 페이지의 일부로 인식하고 실행할 수 있기 때문이다.

 

📷 대응 전략 정리!!

OCR 결과를 안전하게 처리하기 위해 다음이 필요하다.

  • OCR 결과 텍스트를 일반 사용자 입력과 동일하게 취급
  • 서버 저장 전 입력값 유효성 검사 및 새니타이징
  • HTML 출력 시 컨텍스트 기반 escape 적용
  • 프론트엔드에서 HTML 직접 삽입 방식 사용 금지
  • third-party script 통제 및 CSP(Content Security Policy) 적용

결국 OCR 보안의 핵심은 OCR 결과 텍스트를 신뢰하지 않는 것이다.

 


2. React와 연관 지어 보자

🤔 React는 XSS에 자유롭다?

React는 기본적으로 XSS에 강한 프레임워크다. JSX에서 렌더링되는 값은 HTML 문자열로 그대로 삽입되지 않고, 모두 escape된 텍스트 노드로 처리된다.

 

그래서 사용자 입력이나 서버 응답을 JSX에 그대로 출력하더라도, 브라우저에서 스크립트로 실행되는 일은 거의 없다. 이 때문에 React를 쓰면 XSS는 신경 안 써도 된다는 인상을 받기 쉽다.

 

하지만 이 안전성은 React가 의도한 방식으로 렌더링할 때만 유지된다…!!!!

 

dangerouslySetInnerHTML을 사용하거나, ref를 통해 DOM에 직접 접근해 innerHTML을 조작하는 순간, React의 XSS 방어는 더 이상 개입하지 않는다. 이 시점부터 React는 이 HTML이 안전한지에 대해 아무것도 보장하지 않으며, 외부에서 들어온 값이 그대로 DOM에 삽입될 수 있다.

 

그래서 OCR 결과, 서버에서 내려온 HTML, 사용자 입력을 HTML로 렌더링해야 하는 경우에는 반드시!!!!
DOMPurify나 sanitize-html를 이용해 정제 과정을 거쳐야 한다.

 

🤔 React의 state는 그냥 평문 저장소이다.

여기서 한 단계 더 나아가 보면, 문제는 XSS뿐만이 아니다.

React의 state 자체가 민감 정보 관점에서는 꽤 취약한 위치에 있다. React의 state는 암호화된 저장소도 아니고, 보호된 영역도 아니다. 단순히 메모리에 올라간 평문 데이터일 뿐이다…!!!!

 

예를 들어 OCR 라이브러리로 신분증이나 건강 정보에서 텍스트를 추출한 뒤, 그 결과를 state에 저장했다면, 그 순간부터 해당 정보는 메모리에 남아 있고 렌더링 대상이 된다.

개발자 도구를 통해 노출될 수도 있고, 의도치 않은 리렌더링을 통해 화면에 다시 나타날 수도 있다.

 

전략은 저장이 아니라 통과다!!

 

민감 정보(OCR 결과 등)는 필요한 순간에만 로컬 변수 또는 최소한의 state로 잠깐 다루고, 사용자 확인이 끝나면 즉시 서버로 전송한 뒤 state를 null로 초기화한다.

이후 해당 컴포넌트는 언마운트되어 더 이상 렌더링되지 않도록 한다.

 

범위와 시간을 최소화하기

1. 전역 상태나 상위 컴포넌트에는 두지 않는다
2. 정말 필요한 컴포넌트 내부에서만 관리한다
3. 사용이 끝나는 즉시 null로 초기화한다

 

🤔 Re-render는 민감 정보 재노출 이벤트이다.

특히 React의 리렌더링 특성은 민감 정보 관점에서 주의가 필요하다.

React에서는 부모 컴포넌트가 리렌더되면, props가 바뀌지 않았더라도 자식 컴포넌트가 다시 계산된다.

 

민감 정보를 표시하는 컴포넌트가 있다면, 리렌더링은 곧 그 정보가 DOM에 다시 그려진다는 의미다. 겉보기에는 화면 변화가 없더라도, DOM이 재생성되는 과정 자체가 노출 기회를 늘린다.

 

스크린 리더, 브라우저 확장 프로그램, 혹은 잠재적인 악성 스크립트 입장에서는 한 번 더 접근할 수 있는 순간이 되는 셈이다.

그래서 민감 정보를 다룰 때는 성능 최적화 관점이 아니라 노출 최소화 관점에서 React를 바라볼 필요가 있다.

 

해당 컴포넌트를 React.memo로 감싸 props가 실제로 바뀌지 않는 한 다시 렌더링되지 않도록 한다.
필요하다면 useMemo를 사용해 민감 데이터를 기반으로 한 계산 결과도 고정한다!!

 


ETC

https://alwaysbeen.tistory.com/57

 

https://velog.io/@imphj3/JavaScript-tesseract.js를-통해-이미지-한글-텍스트-추출하기

https://bbin-guuuu.tistory.com/88

'FE' 카테고리의 다른 글

인증/인가 전략  (0) 2026.03.01
drop-down 공통 컴포넌트 구현  (0) 2026.02.18
button 공통 컴포넌트 구현  (0) 2026.02.16
마이페이지 min-height 문제  (0) 2026.02.10
ticker 구현기  (0) 2026.02.09