웹 서비스를 개발할 때 인증 기능만 구현했다고 해서 안전한 서비스가 되는 것은 아닙니다
사용자가 로그인된 상태를 악용하는 CSRF,악성 스크립트를 삽입하는 XSS,화면을 속여 의도하지 않은 클릭을 유도하는 Clickjacking과 같은 공격이 어떤 방식인지 알고 해당 공격에 대해 대응할 수 있어야 합니다
CSRF (Cross-Site Request Forgery)
사이트 간 요청 위조 공격으로 인증된 사용자가 특정 서비스에 로그인된 상태에서 악성 사이트에 접속했을 때 공격자가 사용자의 브라우저를 이용하여 사용자 몰래 원치 않는 요청을 서버에 보내도록 만드는 공격
CSRF가 공격이 가능한 이유는 브라우저가 요청을 보낼 때 해당 도메인의 쿠키를 자동으로 포함하여 서버에 전송하기 때문입니다서버가 요청의 출처를 검증하지 않으면 공격자가 만든 요청도 정상 사용자의 요청으로 인식하여 처리되는 취약점을 통해 공격이 이루어집니다
동일 사이트 요청(Same-site request)
- 웹사이트 페이지가 HTTP 요청을 다시 웹사이트로 보내는 것
사이트 간 요청(cross-site request)
- 요청이 다른 웹 사이트로 전송되는 경우 페이지의 출처와 요청이 가는 곳이 다른 요청
공격 구성 요소
- 타겟 웹사이트 (service.com)
- 피해자 사용자 (service.com에 로그인된 상태)
- 인증 세션 (브라우저에 저장된 쿠키)
- 악성 웹 사이트
공격 흐름
- 피해자가 service.com에 로그인
- service.com은 피해자 브라우저에 세션 쿠기를 저장
- 피해자가 로그인 상태를 유지한 채 attack.com에 접속
- attack.com에는 service.com에 요청을 보내는 코드가 숨겨져 있음
- 피해자 브라우저가 service.com으로 요청을 보낸다
- 브라우저는 service.com의 쿠키를 자동으로 포함하여 전송
- service.com 서버는 해당 쿠키를 받아 정상 로그인 사용자 요청으로 판단
- CSRF 방어가 없는 상태라면 친구 추가,비밀번호 변경,게시글 작성 등 인증이 필요한 요청이 사용자 의도와 무관하게 처리
HTTP GET 서비스에 대한 CSRF 공격
GET 요청은 사용자가 버튼을 누르지 않아도 img,iframe,script,link 같은 HTML 태그의 URL 로딩만으로 자동 발생 할 수 있어 서버가 상태 변경 기능을 GET으로 요청을 HTTP Method를 설정하면 CSRF에 취약
1. img 태그를 통한 GET CSRF 공격 구조
공격자는 악성 웹사이트에 이런 코드를 작성하여 공격을 시도합니다
<img src="https://target.com/friends/add?targetUserId=attacker" style="display:none;">
피해자가 이미 target.com에 로그인한 상태에서 공격자 사이트에 접속하면 브라우저는 이미지를 불러오려고 합니다
하지만 src에 들어간 URL은 이미지 경로가 아닌 친구 추가 URL입니다
브라우저는 해당 URL로 쿠키를 포함하여 GET 요청을 보내게 되며 서버는 쿠키를 확인하고 로그인 사용자가 친구 추가를 하는 요청을 받아들여 피해자는 의도하지 않았던 공격자와 친구가 될 수 있습니다
2. iframe 태그를 통한 GET CSRF 공격 구조
img 태그 공격과 비슷하게 악성 웹사이트에 이런 코드를 작성하여 공격을 시도합니다
<iframe src="https://target.com/friends/add?targetUserId=attacker"
style="display:none;">
</iframe>
피해자가 악성 페이지에 접속하면 브라우저는 iframe src의 URL 페이지를 로드하려고 시도합니다
그 과정에서 GET 요청이 자동으로 발생하며 사용자는 화면에서 상호작용을 하지 않고 브라우저가 자동으로 타겟 사이트로 요청을 보내 피해자가 의도하지 않았던 요청을 보내게 됩니다
HTTP POST 서비스에 대한 CSRF 공격
POST 기반 공격 방식은 단순 URL 로딩으로 보내기 어렵기 때문에 공격자가 HTML form을 동적으로 만들고 자동 제출하는 방식을 사용합니다
1. 동적으로 form을 생성 및 자동 submit 공격 구조
예를 들어 타겟 사이트에 친구 추가 API가 있다고 가정
POST <https://target.com/friends/add> targetUserId=attacker
공격자는 악성 웹사이트에 이런 구조로 JavaScript 코드를 작성하여 공격을 시도합니다
function sendCsrfRequest() {
const form = document.createElement("form");
form.method = "POST";
form.action = "<<a href=https://target.com/friends/add>https://target.com/friends/add</a>>";
const input = document.createElement("input");
input.type = "hidden";
input.name = "targetUserId";
input.value = "attacker";
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
window.onload = sendCsrfRequest;
피해자가 target.com에 로그인된 상태에서 공격자 사이트에 접속하면 페이지가 로드되면서 JavaScript 함수가 자동 실행
그 결과 form이 자동 제출되고, 브라우저는 타겟 사이트로 POST 요청을 보냅니다
여기서 hidden 필드는 사용자 화면에는 보이지 않지만, form이 제출될 때 서버로 함께 전송됩니다
POST 방식이라고 해서 CSRF 공격이 불가능한 것은 아니며 공격자는 form을 동적으로 생성하고 hidden input에 원하는 값을 넣어 페이지 로드 시 자동 submit을 실행하는 코드를 통해서 POST 요청을 자동을 보낼 수 있기 때문에 CSRF 공격 대응책을 적용해야 한다
CSRF 대응책
CSRF의 근본적인 원인
- 브라우저가 대상 사이트 쿠키를 자동으로 요청에 포함함
- 서버가 요청이 실제 서비스 화면에서 발생했는지 확인하지 않음
- 서버가 사용자가 직접 수행한 요청인지 확인하지 않음
1. CSRF Token 사용
서버가 사용자 세션마다 예측하기 어려운 토큰을 발급하고 상태 변경 요청마다 이 토큰을 함께 포함하여 전송하도록 합니다
- 서버는 요청이 오면 세션에 저장된 토큰과 요청으로 전달된 토큰을 비교
- 공격자는 정상 사이트가 발급한 CSRF Token 값을 알 수 없어 요청을 방어할 수 있다
2. SameSite 쿠키 설정
쿠키에 SameSite 옵션을 설정하면 외부 사이트에서 발생한 요청에 인증 쿠키가 자동으로 포함되는 것을 제한할 수 있습니다
Set-Cookie: JSESSIONID=abc123; HttpOnly; Secure; SameSite=Lax
주요 옵션은 다음과 같습니다
| 옵션 | 설명 |
| Strict | 사이트 간 요청에는 쿠키를 거의 전송하지 않음 |
| Lax | 외부 링크 이동 같은 일부 GET 요청에는 쿠키 전송, POST 같은 요청은 제한 |
| None | 사이트 간 요청에도 쿠키 전송, Secure 필수 |
Spring Boot HttpOnly, Secure, SameSite를 활용한 쿠키 보안 설정

위 코드는 쿠키 이름과 값을 전달받아 보안 옵션이 적용된 ResponseCookie 객체를 생성하는 메서드입니다
HttpOnly 옵션을 활성화하면 JavaScript에서 해당 쿠기에 접근할 수 없습니다
브라우저의 document.cookie를 통해 쿠키 값을 읽을 수 없기 때문에 XSS 공격이 발생하더라도 공격자가 쿠키에 저장된 토큰을 직접 탈취하기 어려워집니다
Secure 옵션은 HTTPS 환경에서만 쿠키가 전송되도록 설정합니다
이 옵션을 사용하면 HTTP 요청에서는 쿠키가 전송되지 않기 때문에 네트워크 구간에서 쿠키가 노출될 위험을 줄일 수 있습니다
SameSite 옵션은 다른 사이트에서 발생한 요청에 쿠키를 함께 보낼지 제어하는 설정입니다
SameSite=Lax는 기본적인 CSRF 공격을 줄이는 데 도움이 됩니다 일반적인 링크 이동이나 GET 요청에서는 쿠키가 전송될 수 있지만 외부 사이트에서 form submit 등을 통해 발생하는 위험한 요청에는 쿠키 전송이 제한됩니다
3. Origin / Referer Header 검증
요청이 생성된 웹 페이지의 주소를 식별하는 HTTP 헤더 필드를 통해 실제 내 사이트에서 시작된 것인지 확인하는 방법입니다
정상 요청
Origin: <https://service.com>
Referer: <https://service.com/users/100>
공격 사이트에서 온 요청
Origin: <https://evil.com>
Referer: <https://evil.com/attack.html>
하지만 해당 필드는 개인 정보 보호 문제를 일으키는 검색 기록의 일부를 나타내므로 이 필드는 대부분 헤더에서 제거된다
XSS (Cross-Site Scripting)
교차 사이트 스크립팅 공격으로 공격자가 웹 페이지에 악성 스크립트를 삽입하고 피해자의 브라우저에서 해당 스크립트가 실행되로 록 만드는 공격입니다
공격자는 타겟 웹 사이트를 통해 피해자의 브라우저에 자신의 악성 코드를 주입하는 방식으로 악성 코드가 웹 사이트에서 제공되도록 하여 신뢰할 수 있는 요청으로 간주되어 페이지의 콘텐츠에 접근하여 변경하거나 쿠키를 읽어 사용자 대신 요청을 보내는 공격 구조를 가진다
비지속(non-persistent) XSS 공격
- 악성 스크립트가 서버에 저장되지 않고 URL 파라미터나 요청값이 응답에 반사되어 실행되는 공격
지속(Persistent) XSS 공격
- 악성 스크립트가 게시글,댓글처럼 서버에 저장된 뒤 다른 사용자가 조회할 때 실행되는 공격
XSS 공격 구성 요소
- 타겟 웹사이트 service.com
- 피해자 사용자
- 악성 스크립트
- 공격자 사용자 또는 악성 입력값
- 입력값을 검증하지 않고 출력하는 웹 페이지
비지속 XSS 공격 흐름
- 공격자가 악성 스크립트가 포함된 URL을 생성
- 피해자에게 해당 URL 클릭을 유도
- 피해자가 URL에 접속
- 서버가 URL 파라미터 값을 검증 없이 응답 페이지에 출력
- 피해자 브라우저에서 악성 스크립트가 실행
- 쿠키 탈취, 사용자 정보 탈취, 임의 요청 등의 피해가 발생할 수 있음
예시 흐름: /search?keyword=<script>악성코드</script>
지속 XSS 공격 흐름
- 공격자가 게시글, 댓글, 프로필 등에 악성 스크립트를 입력
- 서버가 해당 값을 검증 없이 DB에 저장
- 다른 사용자가 해당 게시글이나 댓글을 조회
- 저장되어 있던 악성 스크립트가 HTML에 포함되어 출력
- 피해자 브라우저에서 악성 스크립트가 실행
- 여러 사용자에게 반복적으로 피해가 발생할 수 있음
XSS는 공격자가 웹 페이지에 악성 스크립트를 삽입하고, 피해자의 브라우저에서 해당 스크립트가 실행되도록 만들어 쿠키 탈취, 사용자 정보 탈취, 임의 요청 전송 등을 수행하는 공격
다른 사람과 친구가 되기 위한 XSS 공격
사용자 입력값을 검증하거나 이스케이프 하지 않고 HTML에 그대로 출력하면 공격자가 삽입한 JavaScript가 다른 사용자의 브라우저에서 실행될 수 있다
1. 취약한 댓글 출력 코드
공격자가 댓글에 이런 값을 입력합니다
<script>
fetch("/friends/add", {
method:"POST",
credentials:"include",
headers: {
"Content-Type":"application/json"
},
body:JSON.stringify({
targetUserId:10
})
});
</script>
여기서 targetUserId: 10은 공격자의 사용자 ID라고 볼 수 있습니다
문제는 서버나 프론트엔드가 댓글 내용을 그대로 HTML로 출력하는 경우 단순 문자열이 아닌 HTML/JavaScript로 해석될 수 있다
2. 어떤 취약점이 존재
출력 이스케이프 미적용 취약점으로 사용자 입력값을 화면에 출력할 때 아래처럼 처리해야 하는데
<script>...</script>
그대로 출력해 버리면 브라우저가 <script>를 코드로 인식합니다
3. 왜 친구 추가 요청이 가능한가?
피해자가 service.com에 로그인되어 있다면 브라우저에는 인증 쿠키가 있습니다
Cookie: JSESSIONID=abc123
악성 스크립트가 service.com 페이지 내부에서 실행되면 다음 요청을 보낼 수 있습니다
fetch("/friends/add", {
method:"POST",
credentials:"include",
body: ...
});
이 요청은 피해자의 브라우저에서 전송되므로 브라우저가 인증 쿠키를 함께 보낼 수 있습니다
자체 전파 XSS 웜(Self-Propagating XSS Worm)
XSS 취약점을 이용해서 악성 스크립트가 한 사용자에게만 실행되는 것이 아니라 실행된 사용자의 계정이나 게시글·프로필 등을 통해 다른 사용자에게 계속 복제·전파되는 공격입니다
일반 XSS는 보통 피해자 브라우저에서 악성 스크립트가 한 번 실행됩니다
공격자 입력 → 피해자 페이지에서 스크립트 실행
하지만 자체 전파 XSS 웜은 여기서 한 단계 더 나아갑니다
공격자 입력
→ 피해자 A 브라우저에서 스크립트 실행
→ 피해자 A의 프로필/게시글/댓글에 같은 스크립트가 자동 삽입
→ 피해자 B가 A의 페이지 조회
→ 피해자 B 브라우저에서도 실행
→ 피해자 B의 페이지에도 복제
→ 계속 확산
악성 스크립트가 자기 자신을 다른 위치에 다시 저장하면서 퍼지는 구조입니다
1. 예시 상황
SNS 서비스에 프로필 소개 기능이 있다고 가정합니다
프로필 소개: 안녕하세요
그런데 서비스가 프로필 소개 내용을 이스케이프하지 않고 그대로 출력합니다
취약한 출력 예시
<divclass="profile-intro">
사용자 입력값 그대로 출력
</div>
이 경우 공격자가 프로필 소개에 악성 스크립트를 넣을 수 있습니다
그 스크립트는 단순히 alert를 띄우는 것이 아니라 피해자의 권한으로 프로필 수정 요청을 보내서 자기 자신과 같은 스크립트를 피해자 프로필에도 저장할 수 있습니다
그래서 피해자의 프로필을 방문한 또 다른 사용자에게도 같은 스크립트가 실행됩니다
2. 전파 구조
자체 전파 XSS 웜은 보통 이런 구조를 가집니다
1. 악성 스크립트 실행
2. 현재 로그인한 사용자 정보 확인
3. 사용자의 프로필, 게시글, 댓글 등 수정 가능한 영역 탐색
4. 같은 악성 스크립트를 해당 영역에 저장
5. 다른 사용자가 그 영역을 조회
6. 같은 과정 반복
핵심은 스크립트가 실행만 되는 것이 아니라 자기 자신을 저장 가능한 위치에 다시 삽입한다는 점입니다
대응책
1. 출력 이스케이프
가장 기본 대응책으로 사용자 입력값을 HTML로 해석하지 않고 문자열로 출력해야 합니다
<div>{profileIntro}</div>
React에서는 위처럼 출력하면 기본적으로 이스케이프가 적용됩니다
2. 필터 적용
일부 HTML을 허용해야 한다면 JavaScript 코드를 필터링할 수 있는 오픈 소스 라이브러리 사용
ClickJacking 공격
사용자가 실제로 클릭하는 대상과 화면에서 보이는 대상을 다르게 만들어 사용자가 의도하지 않은 버튼이나 링크를 클릭하게 만드는 공격
사용자는 “이벤트 참여” 버튼을 누른다고 생각하지만 실제로는 투명하게 덮여 있는 다른 사이트의 “계정 삭제”, “결제 승인”, “권한 허용” 버튼을 누르게 되는 방식입니다
공격 구성 요소
- 타겟 웹사이트 service.com
- 피해자 사용자 service.com에 로그인된 상태
- 악성 웹사이트 attack.com
- 투명하거나 숨겨진 iframe
- 사용자를 속이기 위한 가짜 버튼 또는 화면
공격 원리
공격자는 악성 웹사이트에 타겟 웹사이트를 iframe으로 삽입합니다
<iframesrc="https://service.com/account/delete"
style="opacity:0; position:absolute; top:0; left:0;">
</iframe>
<button>무료 쿠폰 받기</button>
이때 iframe은 투명하게 만들어 사용자의 눈에는 보이지 않게 합니다
사용자는 화면에 보이는 “무료 쿠폰 받기” 버튼을 누른다고 생각하지만, 실제 클릭 위치에는 투명한 iframe 안의 타겟 웹사이트 버튼이 겹쳐져 있을 수 있습니다
결과적으로 사용자는 의도하지 않았지만 service.com의 특정 버튼을 클릭하게 됩니다
대응책
1. X-Frame-Options 설정
외부 사이트에서 내 페이지를 iframe으로 삽입하지 못하게 막습니다.
X-Frame-Options: DENY
또는 같은 사이트에서만 iframe을 허용할 수도 있습니다.
X-Frame-Options: SAMEORIGIN
옵션 의미
| DENY | 어떤 사이트에서도 iframe으로 삽입 불가 |
| SAMEORIGIN | 같은 출처에서만 iframe 삽입 가능 |
2. CSP frame-ancestors 설정
최근에는 CSP의 frame-ancestors를 많이 사용합니다.
Content-Security-Policy: frame-ancestors 'none';
이 페이지는 어떤 사이트에서도 iframe으로 포함될 수 없다
같은 출처만 허용하려면 다음처럼 설정합니다
Content-Security-Policy: frame-ancestors 'self';
Spring Boot 기반 서비스를 개발할 때는 인증 기능뿐만 아니라 쿠키 보안 옵션, CSRF Token, Origin/Referer 검증, 출력 이스케이프, 보안 헤더 설정 등을 함께 고려해야 합니다
이러한 기본적인 보안 설정을 적용하는 것만으로도 웹 서비스의 보안 수준을 크게 높일 수 있습니다