개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › 보안 › 웹 공격과 방어

클릭재킹과 오픈 리다이렉트

내 화면이 남의 페이지에 숨겨지거나, 내 주소가 사용자를 남의 사이트로 넘겨주는 일을 어떻게 막나

클릭재킹은 다른 사이트가 내 사이트의 화면을 보이지 않는 프레임에 넣어, 사용자가 모르는 사이에 내 화면의 단추를 누르게 만드는 것입니다. 오픈 리다이렉트는 "로그인 뒤 돌아갈 주소"를 요청에서 받아 확인 없이 그대로 보내 주어, 내 사이트의 주소로 시작하는 링크가 다른 사이트로 넘어가게 되는 것입니다. 둘 다 서버가 뚫리는 것이 아니라 내 사이트에 대한 믿음이 이용당하는 문제이며, 클릭재킹은 서버가 보낸 응답 헤더를 보고 브라우저가 막고, 오픈 리다이렉트는 서버가 주소를 검사해 막습니다.

실행으로 대조 · 스프링 부트 4.1.1(스프링 시큐리티 7.1.1) 서버의 실제 응답 헤더와 스물네 가지 조합을 대조하고, 브라우저의 동작은 크로미움 151로 확인. 다른 브라우저는 문서 기준

이 실습에 나오는 개념

클릭재킹
다른 사이트가 내 사이트의 화면을 프레임(iframe)에 넣고 보이지 않게 만든 뒤, 사용자가 다른 것을 누른다고 믿게 하여 내 화면의 단추를 누르게 만드는 공격이다. 사용자의 브라우저가 직접 누른 것이므로 서버가 받는 요청은 정상적인 클릭과 구별되지 않는다. 그래서 서버는 요청을 가려내는 대신, 응답 헤더로 "이 화면을 프레임에 넣지 말라"고 브라우저에 알려 막는다. 막는 일을 실제로 하는 쪽은 브라우저이므로, 헤더가 빠진 화면은 누구든 프레임에 넣을 수 있다.
X-Frame-Options와 frame-ancestors
응답을 받은 브라우저에게 이 화면을 누구의 프레임에 넣어도 되는지 알려 주는 두 가지 방법이다. 하나는 X-Frame-Options 헤더이고, 다른 하나는 Content-Security-Policy 헤더 안에 적는 frame-ancestors 지시어다. X-Frame-Options는 DENY(어디에도 넣지 않음)와 SAMEORIGIN(출처가 같은 페이지에만) 두 값을 쓰고, Content-Security-Policy의 frame-ancestors는 넣어도 되는 출처를 목록으로 적어 특정한 다른 사이트만 허용할 수 있다. 스프링 시큐리티는 따로 설정하지 않아도 X-Frame-Options: DENY를 붙인다. 둘이 함께 오면 요즘 브라우저는 frame-ancestors를 따르고 X-Frame-Options는 무시하므로, 둘의 뜻이 어긋나지 않게 맞춰 두어야 한다.
오픈 리다이렉트
서버가 요청에 실려 온 주소를 확인하지 않고 그 주소로 이동하라고 응답하는 취약점이다. "로그인 뒤 돌아갈 주소"처럼 편의를 위해 만든 기능에서 생기며, 내 사이트의 주소로 시작하는 링크가 다른 사이트로 넘어가는 통로가 된다. 사용자는 주소의 앞부분을 보고 믿으므로 가짜 로그인 화면으로 끌어들이는 데 쓰인다. 막으려면 같은 사이트의 경로처럼 받아도 되는 값만 받아들이고, //로 시작하는 주소가 다른 호스트를 가리킨다는 점까지 검사해야 한다. 주소 대신 미리 정해 둔 이름을 받아 서버의 목록에서 주소를 찾는 방법이 더 확실하다.
Spring Security와 SecurityFilterChain
Spring Security는 인증과 인가를 처리하는 스프링의 보안 프레임워크로, 서블릿 필터들의 묶음으로 동작한다. SecurityFilterChain은 어떤 요청에 어떤 보안 필터들을 어떤 순서로 적용할지 정의한 것이다. 요청이 컨트롤러에 도달하기 전에 필터들이 차례로 인증 정보 확인, 권한 검사 등을 수행한다.
CORS와 동일 출처 정책
동일 출처 정책은 브라우저가 한 출처(스킴, 호스트, 포트의 조합)의 스크립트가 다른 출처의 응답을 읽지 못하게 막는 보안 정책이다. CORS는 서버가 응답 헤더로 특정 출처의 접근을 허용한다고 알려 이 제한을 풀어 주는 방법이다. 브라우저가 적용하는 정책이므로 서버끼리의 호출이나 도구를 이용한 호출에는 해당하지 않는다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 사용자가 https://attacker.example의 페이지를 엽니다. 화면에는 "경품 받기" 단추만 보입니다. 그런데 이 페이지는 그 단추 위에 shop.example의 결제 화면을 프레임(iframe)으로 겹쳐 놓고, 보이지 않게 투명하게 만들어 두었습니다.
  2. 브라우저가 프레임에 넣을 화면을 받으려고 https://shop.example/pay를 요청합니다. 프레임 안의 화면도 주소창에 주소를 넣었을 때와 같은 방식으로 서버에서 받아 옵니다.
  3. shop.example이 결제 화면을 응답합니다. 설정에서 frameOptions를 꺼 두었으므로, 응답에는 프레임에 관한 헤더가 하나도 없습니다.
  4. 브라우저가 응답 헤더를 확인합니다: 프레임에 넣지 말라는 표시가 없습니다. 이런 화면은 어느 사이트의 페이지든 프레임에 넣을 수 있으므로, 브라우저가 결제 화면을 프레임 안에 그립니다.
  5. 사용자가 "경품 받기"를 누릅니다: 눈에 보이는 것은 공격자 페이지의 단추지만, 그 위에는 투명한 결제 화면이 겹쳐 있습니다. 실제로 눌린 것은 shop.example의 [결제하기] 단추입니다. 사용자가 shop.example에 로그인된 상태였다면 자기도 모르게 결제를 승인한 것이 됩니다.

실행 결과

코드

// shop.example 서버의 스프링 시큐리티 설정
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // (누가 들어올 수 있는지에 관한 설정은 생략)
    http.headers(h -> h.frameOptions(f -> f.disable()));
    return http.build();
}

// 이 설정에서 /pay 응답에 붙는 프레임 관련 헤더
//   (없음)

<!-- 프레임에 넣는 쪽: https://attacker.example 의 페이지 -->
<iframe src="https://shop.example/pay"></iframe>

이 모형의 전제

프레임 관련 헤더 다섯 가지와 Location 헤더 아홉 가지는 스프링 부트 4.1.1(스프링 시큐리티 7.1.1, 내장 톰캣 11) 서버를 127.0.0.1에 띄워 실제 응답과 대조했습니다. 브라우저의 동작(프레임 안에 그리는지 열다섯 가지, Location을 따라 어느 서버에 닿는지 아홉 가지)은 크로미움 151을 화면 없이 실행해 확인했으며, 이때 출처는 127.0.0.1의 서로 다른 포트로 흉내 냈습니다. 다른 브라우저는 실행하지 않았고 문서를 따랐습니다: X-Frame-Options는 RFC 7034와 HTML 표준, frame-ancestors는 W3C의 Content Security Policy Level 3, //로 시작하는 주소는 RFC 3986 4.2절, 역슬래시를 슬래시처럼 다루는 것은 WHATWG URL 표준입니다. 단순화한 것: 프레임은 한 겹만 다뤘습니다(여러 겹이면 브라우저는 바깥의 모든 페이지를 확인합니다). 화면을 투명하게 겹치는 방법은 싣지 않았습니다. SameSite=Lax인 쿠키는 다른 사이트의 프레임 안에서 보내는 요청에 붙지 않고(RFC 6265bis 초안), 크롬 계열 브라우저는 SameSite를 지정하지 않은 쿠키도 Lax로 다룹니다. 그래서 로그인이 필요한 화면의 클릭재킹은 세션 쿠키가 SameSite=None일 때 주로 성립합니다. 쿠키에 관한 이 부분은 실행하지 않았습니다. X-Frame-Options의 ALLOW-FROM은 요즘 브라우저가 지원하지 않아 다루지 않았습니다. Location이 완전한 주소로 바뀌는 것은 스프링 부트의 기본값(server.tomcat.use-relative-redirects=false)일 때이며, 이 값을 true로 바꾼 서버는 받은 값을 그대로 내보냅니다. 그 설정에서는 /\attacker.example 꼴의 값도 "/"로 시작하는지만 보는 검사를 통과해 크로미움이 다른 서버로 이동하는 것을 확인했습니다. 화면의 Location은 시험 서버의 주소를 https://shop.example로 바꿔 적은 것입니다.

앞 실습: CORS · 다음 실습: 정보 누출

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요