클릭재킹이란? 투명한 화면으로 클릭을 유도하는 공격 원리

클릭재킹이 발생하는 원인과 투명한 iframe으로 사용자의 클릭을 가로채는 과정, X-Frame-Options와 CSP를 이용한 방어 방법을 알아봅니다.

웹사이트에서 이벤트 참여나 동영상 재생 버튼을 눌렀는데 실제로는 계정 설정이 변경된다면 어떨까. 사용자는 화면에 보이는 버튼을 눌렀다고 생각하지만, 브라우저에서는 그 위에 겹쳐진 다른 웹페이지의 버튼이 클릭될 수 있다.

이처럼 공격자가 화면을 여러 겹으로 구성해 사용자의 클릭을 의도하지 않은 기능으로 전달하는 공격을 클릭재킹이라고 한다. 사용자의 계정이 로그인된 상태라면 비밀번호를 직접 알아내지 않아도 중요 기능을 실행하도록 유도할 수 있다.

클릭재킹이란?

클릭재킹은 ‘Click’과 ‘Hijacking’을 합친 용어로, 사용자의 클릭을 가로채는 공격을 의미한다. UI Redressing 공격이라고도 부른다.

공격자는 자신이 만든 웹페이지 안에 공격 대상 사이트를 iframe으로 불러온다. 이후 iframe을 투명하게 만들거나 정상적인 버튼 아래에 정확하게 겹쳐 놓는다.

사용자에게는 이벤트 참여, 쿠폰 받기 또는 영상 재생 버튼만 보인다. 하지만 실제 클릭 위치에는 투명한 iframe 속 계정 변경, 결제 승인 또는 게시물 삭제 버튼이 놓여 있을 수 있다.

클릭재킹의 핵심은 사용자에게 보이는 화면과 실제로 클릭되는 화면이 다르다는 점이다.

iframe이란?

iframe은 하나의 웹페이지 안에 다른 웹페이지를 표시하는 HTML 요소다. 지도, 동영상, 결제 화면처럼 외부 콘텐츠를 현재 페이지 안에 삽입할 때 주로 사용한다.

정상적인 iframe 사용 자체는 보안 문제가 아니다. 문제는 중요 기능을 제공하는 웹페이지가 아무 사이트의 iframe 안에서도 열릴 수 있을 때 발생한다.

공격자는 iframe의 위치, 크기와 투명도를 조절할 수 있다. 대상 사이트의 버튼 위치를 미리 확인한 뒤 자신이 만든 가짜 버튼과 같은 위치에 배치하면 사용자의 클릭을 다른 페이지로 전달할 수 있다.

클릭재킹이 발생하는 원인

가장 큰 원인은 웹사이트가 다른 출처의 iframe 안에서 표시되는 것을 제한하지 않은 것이다. HTTP 응답에 X-Frame-Options 또는 CSP의 frame-ancestors 지시문이 없으면 외부 사이트가 해당 페이지를 iframe으로 불러올 수 있다.

로그인한 사용자가 한 번의 클릭만으로 중요 기능을 실행할 수 있는 구조도 위험을 키운다. 계정 삭제, 이메일 변경이나 결제 승인처럼 중요한 작업이 별도의 비밀번호 확인 없이 처리되면 클릭재킹이 실제 피해로 이어질 가능성이 커진다.

상태를 변경하는 기능을 GET 요청으로 구현하는 것도 피해야 한다. 사용자가 특정 주소에 접근하는 것만으로 정보가 수정되면 iframe이나 이미지 태그 등 다양한 방식으로 요청이 발생할 수 있다.

화면 안에서 iframe 여부를 확인해 페이지를 강제로 이동시키는 JavaScript 방식만 사용하는 것도 충분하지 않다. 스크립트가 차단되거나 브라우저 환경에 따라 예상과 다르게 동작할 수 있으므로 HTTP 응답 헤더에서 프레임 사용을 제한해야 한다.

투명한 화면으로 클릭을 유도하는 원리

공격자는 먼저 로그인 후 사용할 수 있는 중요 페이지가 외부 iframe에서 정상적으로 열리는지 확인한다. 계정 설정이나 게시물 관리 페이지가 아무 제한 없이 iframe에 표시된다면 클릭재킹 가능성이 생긴다.

이후 공격 페이지에 대상 사이트를 불러오는 iframe을 추가하고 투명도를 낮춘다. 화면에는 공격자가 만든 정상적인 버튼이나 안내 문구를 표시한다.

개념적인 화면 구성은 다음과 같다.

<div class="fake-button">
    이벤트 참여하기
</div>

<iframe
    src="https://target.example/account/settings"
    class="hidden-frame">
</iframe>
.hidden-frame {
    position: absolute;
    top: 100px;
    left: 100px;
    width: 500px;
    height: 400px;
    opacity: 0;
}

사용자는 화면에 보이는 이벤트 버튼을 누르지만, 실제 클릭 좌표에는 투명한 iframe의 중요 버튼이 놓여 있을 수 있다.

브라우저는 iframe 안의 대상 사이트에 해당 사이트의 쿠키를 전송할 수 있다. 사용자가 대상 서비스에 로그인된 상태라면 서버는 로그인 사용자가 직접 보낸 요청으로 판단할 가능성이 있다.

다만 실제 쿠키 전송 여부는 SameSite 속성, 브라우저 정책과 요청 방식에 따라 달라진다. 이러한 차이가 있더라도 프레임 삽입 자체를 차단하는 설정은 별도로 적용해야 한다.

클릭재킹으로 발생할 수 있는 피해

클릭재킹 공격이 성공하면 사용자는 자신의 의도와 다른 기능을 실행할 수 있다. 게시물 삭제, 설정 변경, 구독 신청과 같은 작업이 대표적이다.

관리자 페이지가 iframe에서 열리고 중요 기능에 추가 인증이 없다면 관리자 계정을 이용한 변경 작업으로 피해가 커질 수도 있다. 카메라나 마이크 사용 권한, 파일 선택과 관련된 화면을 교묘하게 배치하는 공격도 고려해야 한다.

한 번의 클릭으로 작업이 완료되지 않더라도 공격자는 여러 단계의 화면을 순서대로 보여주며 여러 번 클릭하도록 유도할 수 있다. 이를 다단계 클릭재킹이라고 한다.

클릭재킹은 사용자의 아이디와 비밀번호를 직접 탈취하는 공격과는 다르다. 이미 로그인된 사용자의 세션과 클릭 행동을 이용해 정상 기능을 실행하게 만든다는 점이 핵심이다.

X-Frame-Options를 이용한 방어 방법

X-Frame-Options는 현재 웹페이지가 iframe, frame 또는 object 안에 표시될 수 있는지를 제어하는 HTTP 응답 헤더다.

외부 사이트뿐 아니라 같은 사이트에서도 iframe 사용이 필요하지 않다면 다음과 같이 설정한다.

X-Frame-Options: DENY

DENY는 모든 출처에서 해당 페이지를 프레임 안에 표시하지 못하게 한다.

같은 출처에서만 iframe을 사용해야 한다면 다음 설정을 사용할 수 있다.

X-Frame-Options: SAMEORIGIN

SAMEORIGIN은 현재 페이지와 동일한 출처의 문서에서만 프레임 표시를 허용한다.

과거에는 특정 주소를 허용하기 위한 ALLOW-FROM 값도 사용됐지만 브라우저 지원이 일관되지 않아 권장되지 않는다. 특정 출처를 허용해야 한다면 CSP의 frame-ancestors를 사용하는 것이 적절하다.

CSP frame-ancestors 설정 방법

CSP의 frame-ancestors 지시문은 어떤 출처가 현재 페이지를 프레임으로 불러올 수 있는지 지정한다.

프레임 표시가 필요하지 않은 서비스는 다음과 같이 설정할 수 있다.

Content-Security-Policy: frame-ancestors 'none'

'none'은 모든 출처의 프레임 삽입을 차단한다.

같은 출처에서만 허용하려면 다음과 같이 설정한다.

Content-Security-Policy: frame-ancestors 'self'

특정 협력사 사이트의 프레임을 허용해야 한다면 허용할 출처를 정확하게 등록할 수 있다.

Content-Security-Policy:
    frame-ancestors 'self' https://partner.example

frame-ancestors는 현재 페이지를 누가 프레임 안에 넣을 수 있는지 정하는 지시문이다. 현재 페이지가 어떤 iframe 콘텐츠를 불러올 수 있는지를 지정하는 frame-src와는 목적이 다르므로 혼동하지 않아야 한다.

호환성을 고려해 CSP의 frame-ancestorsX-Frame-Options를 함께 적용할 수 있다. 두 설정은 서로 모순되지 않도록 동일한 정책으로 구성하는 것이 좋다.

Spring Security 클릭재킹 방어 설정

Spring Security에서는 보안 헤더 설정을 통해 프레임 표시를 제한할 수 있다. 사이트 전체에서 iframe을 사용할 필요가 없다면 다음과 같이 설정할 수 있다.

@Bean
SecurityFilterChain securityFilterChain(
        HttpSecurity http) throws Exception {

    http.headers(headers ->
        headers
            .frameOptions(frame ->
                frame.deny()
            )
            .contentSecurityPolicy(csp ->
                csp.policyDirectives(
                    "frame-ancestors 'none'"
                )
            )
    );

    return http.build();
}

이 설정은 X-Frame-Options: DENY와 CSP의 frame-ancestors 'none' 정책을 적용한다. 브라우저는 다른 페이지가 해당 웹사이트를 iframe으로 불러오려고 할 때 표시를 차단한다.

같은 출처에서 iframe이 필요한 서비스라면 frame.deny() 대신 frame.sameOrigin()을 사용하고 CSP도 frame-ancestors 'self'로 맞출 수 있다.

http.headers(headers ->
    headers
        .frameOptions(frame ->
            frame.sameOrigin()
        )
        .contentSecurityPolicy(csp ->
            csp.policyDirectives(
                "frame-ancestors 'self'"
            )
        )
);

프레임이 필요한 일부 페이지만 예외로 처리해야 한다면 사이트 전체의 보호 설정을 해제하지 말고, 해당 응답에만 필요한 출처를 명확하게 지정하는 것이 좋다.

중요 기능에는 재인증이 필요하다

프레임 삽입을 차단하더라도 계정 삭제, 비밀번호와 이메일 변경, 결제처럼 중요한 기능에는 추가 인증 절차를 적용해야 한다.

사용자의 현재 비밀번호를 다시 확인하거나 일회용 인증번호, MFA를 요구하면 로그인 세션만으로 중요 작업이 완료되는 것을 막을 수 있다.

처리 전 최종 확인 화면을 제공하는 것도 도움이 된다. 다만 단순한 확인 버튼 하나만 추가하면 공격자가 해당 버튼의 위치에 다시 투명한 화면을 겹칠 수 있다. 확인 화면은 작업 내용과 대상을 명확하게 보여주고 중요한 경우 재인증까지 요구해야 한다.

상태를 변경하는 요청에는 POST, PUT 또는 DELETE처럼 목적에 맞는 HTTP 메서드를 사용하고 CSRF 토큰도 검증해야 한다.

SameSite와 CSRF 방어의 역할

SameSite 쿠키 속성은 다른 사이트에서 시작된 요청에 세션 쿠키가 전송되는 범위를 제한한다. 클릭재킹과 결합된 일부 요청의 위험을 줄이는 데 도움이 될 수 있다.

CSRF 토큰은 외부 사이트가 사용자의 세션을 이용해 임의의 요청을 보내는 것을 막는 중요한 방어 수단이다. 상태 변경 요청에 유효한 CSRF 토큰을 요구하면 단순한 요청 위조를 줄일 수 있다.

하지만 SameSite 쿠키와 CSRF 토큰이 클릭재킹 방어 헤더를 완전히 대신하지는 않는다. 사용자가 실제 대상 페이지의 버튼을 클릭하는 상황에서는 페이지 내부에 정상적인 토큰이 이미 포함돼 있을 수 있기 때문이다.

따라서 frame-ancestors, X-Frame-Options, CSRF 방어와 중요 기능 재인증을 함께 적용해야 한다.

JavaScript Frame Busting의 한계

과거에는 현재 페이지가 iframe 안에서 열렸는지 확인한 후 최상위 창으로 이동시키는 JavaScript를 사용하기도 했다.

if (window.top !== window.self) {
    window.top.location = window.self.location;
}

이러한 코드는 보조적인 방어 수단으로 사용할 수 있지만 주요 대책으로 의존해서는 안 된다. 공격 페이지에서 스크립트 실행을 제한하거나 브라우저 동작을 이용해 우회할 가능성이 있기 때문이다.

HTTP 응답 헤더는 브라우저가 페이지를 프레임에 표시하기 전에 정책을 적용한다. 따라서 서버에서 X-Frame-Options와 CSP frame-ancestors를 설정하는 것이 우선이다.

클릭재킹 취약점 점검 항목

클릭재킹을 점검할 때는 로그인 후 사용하는 주요 페이지의 HTTP 응답 헤더를 확인해야 한다. X-Frame-Options 또는 CSP의 frame-ancestors가 누락돼 있는지 살펴본다.

프레임 표시 정책이 일부 페이지에만 적용되고 계정 설정이나 관리자 기능에서는 빠져 있지 않은지도 확인해야 한다. 프록시나 웹 서버 설정에 따라 특정 응답에서 보안 헤더가 제거될 가능성도 있다.

테스트 환경에서는 다른 출처의 페이지에 점검 대상 주소를 iframe으로 넣었을 때 실제 화면이 표시되는지 확인할 수 있다. 단, 본인이 관리하거나 명시적으로 허가받은 시스템에서만 테스트해야 한다.

계정 삭제, 정보 변경과 결제 기능이 한 번의 클릭으로 처리되는지, 재인증과 CSRF 검증이 적용돼 있는지도 함께 점검한다.

자주 묻는 질문

X-Frame-Options와 CSP를 모두 설정해야 하나요?

현대적인 브라우저에서는 CSP의 frame-ancestors가 세밀한 정책을 제공한다. 다양한 브라우저와 기존 환경을 고려한다면 X-Frame-Options도 함께 설정할 수 있다. 두 헤더의 허용 범위가 서로 일치해야 한다.

iframe을 모두 차단하면 문제가 생기지 않나요?

서비스에서 지도, 동영상이나 자체 페이지를 iframe으로 사용한다면 필요한 출처를 확인해야 한다. 현재 페이지를 다른 사이트가 삽입할 필요가 없다면 frame-ancestors 'none'을 적용해도 된다. 같은 출처에서만 필요하다면 'self'를 사용할 수 있다.

CSRF 토큰으로 클릭재킹도 막을 수 있나요?

CSRF 토큰만으로는 충분하지 않다. 클릭재킹에서는 사용자가 iframe 안의 실제 페이지를 직접 클릭하므로 정상 토큰이 포함된 요청이 발생할 수 있다. 프레임 표시를 차단하는 응답 헤더가 필요하다.

화면의 투명도를 차단하면 해결되나요?

공격자는 투명도뿐 아니라 화면 일부를 가리거나 버튼 위치를 바꾸는 다양한 방식을 사용할 수 있다. 공격 화면의 모양을 탐지하기보다 외부 사이트가 중요 페이지를 프레임에 넣지 못하도록 차단해야 한다.

마무리

클릭재킹은 공격자가 정상 웹페이지를 투명한 iframe으로 겹쳐 사용자의 클릭을 의도하지 않은 기능에 전달하는 공격이다. 사용자는 화면에 보이는 버튼을 클릭하지만 실제로는 계정 변경이나 결제 같은 다른 작업을 실행할 수 있다.

가장 중요한 대응 방법은 HTTP 응답에서 프레임 사용을 제한하는 것이다. iframe이 필요하지 않다면 X-Frame-Options: DENYContent-Security-Policy: frame-ancestors 'none'을 적용한다. 같은 출처에서만 필요하다면 SAMEORIGIN'self'를 사용할 수 있다.

클릭재킹 방어의 핵심은 외부 사이트가 중요 페이지를 보이지 않는 화면으로 겹쳐 놓지 못하게 하는 것이다. 프레임 차단 헤더, CSRF 방어, SameSite 쿠키와 중요 기능 재인증을 함께 적용해야 클릭 유도 공격의 위험을 효과적으로 줄일 수 있다.

핵심 키워드: 클릭재킹, Clickjacking, 투명한 iframe, UI Redressing, X-Frame-Options, CSP frame-ancestors, 클릭재킹 공격 원리, 클릭재킹 방어 방법

댓글 남기기