[Spring Boot] 72. 스프링부트 블로그 v3 (RestAPI) (17) Origin & CSRF

김주희's avatar
May 14, 2025
[Spring Boot] 72. 스프링부트 블로그 v3 (RestAPI) (17) Origin & CSRF

Origin & CSRF Security

정상적인 브라우저 요청을 검증하는 방법
웹 서비스를 만들다 보면 “이 요청이 정말 정상적인 브라우저에서 발생한 것인지” 판단해야 할 때가 많다.
특히 로그인, 결제, 민감한 데이터 요청 등에서는 출처(Origin) 를 검증하지 않으면 악성 페이지나 스크립트가 임의로 요청을 보내는 CSRF 공격이 발생할 수 있다. 브라우저는 서버에게 다양한 헤더 값을 보내고, 서버는 이를 이용해 요청의 정상성을 판단한다.
대표적으로 다음 네 가지 값이 가장 많이 활용된다.
  • User-Agent
  • Referer
  • Origin
  • CSRF Token
아래에서 하나씩 살펴보자.

1. User-Agent, Referer, Origin

User-Agent (UA)

  • 요청을 보내는 프로그램 정보
  • 예: Chrome, Safari, 모바일 브라우저 등
  • 서버는 UA를 통해 브라우저인지, 봇인지, 이상한 프로그램인지를 대략 판단할 수 있다.
하지만 코드로 UA 값을 조작할 수 있기 때문에 보안 수단으로 완전하지는 않다.

Referer

  • 사용자가 직전에 어떤 URL에 있었는지 알려주는 값
  • 예:
    • 로그인 페이지 /login-form 에서 로그인 요청이 들어왔다면
      referer = https://blog.com/login-form
이를 통해 서버는
“정상적인 로그인 폼에서 요청이 들어왔구나” 라는 맥락(context)을 이해할 수 있다.
❗ 하지만 Referer 또한 HTTP 요청 코드에서 조작 가능 → 100% 신뢰 불가

Origin (출처)

  • 요청이 발생한 도메인
  • Query, Path 등은 포함되지 않고 오직 프로토콜 + 도메인만 포함
  • 예:
    • https://blog.com/login-form 페이지 → origin = https://blog.com
서버는 Origin 값을 보고 “다른 출처에서 들어온 요청인가?” 를 판단할 수 있다.
즉, CORS 보안의 핵심 값이기도 함.

그런데 문제는…

HTTP 요청은 코드로 만들 수도 있고(fetch, RestTemplate, HttpURLConnection 등), 그 경우 UA, Referer, Origin 모두 조작 가능하다는 점이다. 그래서 이 헤더만으로는 공격을 완전히 막을 수 없다.
여기서 필요한 것이 CSRF Token이다.

2. CSRF Token의 필요성

브라우저 기반 서비스들은 과거부터 폼 요청이 위조되는 공격(CSRF) 에 취약했다.
예:
사용자가 블로그에 로그인한 상태에서 악성 페이지를 열었을 때, 해커가 준비한 스크립트가 자동으로 POST /withdraw 같은 요청을 보낼 수 있다.
이를 방지하기 위해 서버는:
  1. 로그인 폼을 렌더링할 때
  1. 해시 기반으로 전자서명된 CSRF 토큰을 함께 보내준다
  1. 사용자는 폼 제출 시 그 토큰을 서버로 다시 보낸다
  1. 서버는 그 값이 내가 발급한 CSRF 토큰이 맞는지 검증
이 과정 덕분에:
  • 외부 페이지에서 만든 HTML 폼은 올바른 CSRF 토큰을 포함할 수 없고
  • Java 코드로 임의 요청을 보낼 때도 토큰 없이 제출하면 403 Forbidden
즉, CSRF 토큰은 브라우저 기반 정상 요청인지 가장 강력하게 검증하는 수단이다.

3. RestTemplate에서 CSRF를 우회할 수 없는 이유

브라우저 기반 요청과 다르게, Java 코드(RestTemplate)로 HTTP 요청을 보낼 때는 CSRF 검증이 통과되지 않는다.
왜?
  • 서버가 렌더링해준 HTML 페이지를 거치지 않기 때문
  • 즉, 서버가 발급한 CSRF 토큰을 받을 경로가 없음
  • 또한 서버가 발급한 JSESSIONID 쿠키도 당연히 없음
그래서 다음과 같은 수동 설정이 필요하다:
headers.set("X-CSRF-TOKEN", "발급받은 실제 토큰"); headers.set("Cookie", "JSESSIONID=서버에서 받은 실제 쿠키"); headers.set("Referer", "https://example.com/form"); headers.set("User-Agent", "Mozilla/5.0 ...");
→ 하지만 이런 토큰은 브라우저에서만 정상적으로 발급되기 때문에
RestTemplate로 CSRF 보호된 API를 마음대로 공격할 수 없음
결국 CSRF는:
“내가 만든 진짜 페이지에서 발생한 요청인지 확인하기 위한 가장 확실한 방법”

4. 정리

정상적인 요청인지 판단하기 위해 서버가 활용할 수 있는 값들:
역할
한계
User-Agent
브라우저/프로그램 구분
조작 가능
Referer
직전 페이지 검증
조작 가능
Origin
출처 도메인 확인
조작 가능
CSRF Token
서버가 직접 서명한 인증 토큰
브라우저 외에는 발급 불가 → 가장 강력
즉, Origin / Referer / UA만으로는 완전한 보안을 만들 수 없고 최종적으로 CSRF 토큰 + 서버 검증 로직이 필요하다.

결론

UA, Referer, Origin은 “정상 요청인지”를 판별하는 보조 지표이고,
CSRF 토큰만이 진짜 브라우저 기반 정상 요청인지 검증하는 핵심 기술이다.
이 네 가지 값은 함께 쓰여 웹 서비스를 다양한 위조 요청으로부터 보호하는 중요한 도구다.
 
 

1. Origin

 

UA, Refer, Origin,CSRF

다른 출처에서 요청 = 비정상적인 접근에서 요청을 하는 구나 알 수 있음 ⇒ 출처를 알아야 함
header에 레퍼럴을 적용 → 출처와는 다른데 블로그 사이트에서 로그인 폼 요청하면 로그인 페이지가 뜨는데 서버가 로그인 폼이라는 htmlcssjs 데이터를 줌 blog.com/login-form → origin은 blog.com임 뒤에 주소는 중요하지 않음 로그인 폼에서 id와 pw 요청 → 서버가 응답을 주는데 항상 로그인 폼이라는걸 req에 담아서(브라우저가 담아서 감) 서버는 blog.com이라는 출처에서 login-form
레퍼럴 = 직전 주소 → 내 출처는 동일한데 정상적인 화면에서 로그인했구나 를 알 수 잇음
 
referer = 직전 url을 의미 ≠ 직전 도메인 ≠ origin
origin =
host 주소 = client의 ip 주소 = remoteAddr
 
 
서버는 다른 출처에서 오는 요청을 방어할 수 있음 - 필터같은데에서 if로
로그인 정상적 레퍼럴 login-form 아니면 튕겨낼 수 있음
UserAgent의 프로그램 정보가 브라우저인지 확인해야함 아니면 (restApi가 아니고 예전 블로그 기준)
 
UA, Refer, Origin → 정상적 요청인지 알 수 있음
 
http 요청일때는 코드로 요청할 수 있음 = 헤더에 뭐든 넣을 수 있다는 것 UA, Refer 다 변경 가능 = 못 막는다 → 그냥 귀찮게 하는 것 밖에 안된다.
CSR
로그인 폼 그림 서버가 만들어서 클라이언트에게 주는데 뷰 렌더링해서 줄 때 폼의 인풋태그에 전자서명한 해시를 끼워넣어서 준다. (인풋태그에 CSRF에 토큰을 만들어서 집어넣어가지고 전달)
페이지마다 동일한 토큰 안줌 getsalt 같은걸로
refer,ua,origin,csrf 토큰 → 4가지를 확인해서 정상적인 요청인지 판단
java 코드로 http url connection (= fetch) 해서 http 요청하게 되면 csrf 토큰에서 문제 생김 검증이 안되는
서명이 엄청나게 많은 일을 할 수 잇음
내가 만들어준 페이지가 아닌건지 확인가능
 
Q. CSRF 어떻게 만들어?
http 요청 코드로 하는 방법
import org.springframework.http.*; import org.springframework.web.client.RestTemplate; import java.util.*; public class RestTemplateCsrfExample { public static void main(String[] args) { String targetUrl = "https://example.com/submit"; // RestTemplate 객체 생성 RestTemplate restTemplate = new RestTemplate(); // ======================== // 1. 요청 헤더 설정 // ======================== HttpHeaders headers = new HttpHeaders(); // (1) CSRF 토큰 - 서버에서 받은 값이어야 함 headers.set("X-CSRF-TOKEN", "AbCdEfGhIj123456"); // 수동 입력 // (2) JSESSIONID - 서버에서 받은 쿠키값이어야 함 headers.set("Cookie", "JSESSIONID=abcde12345xyz"); // (3) Referer headers.set("Referer", "https://example.com/form"); // (4) User-Agent headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); // (5) Content-Type headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); // ======================== // 2. 요청 바디 설정 // ======================== MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); body.add("amount", "1000"); body.add("to", "receiver_account"); // ======================== // 3. HttpEntity 만들기 // ======================== HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers); // ======================== // 4. 요청 실행 // ======================== ResponseEntity<String> response = restTemplate.exchange( targetUrl, HttpMethod.POST, requestEntity, String.class ); // ======================== // 5. 응답 출력 // ======================== System.out.println("Response Code: " + response.getStatusCode()); System.out.println("Response Body: " + response.getBody()); } }
⇒ JS로는 장난 못치게 하려고!
Share article

jay0628