탭내빙 공격이 발생하는 원인과 새 창으로 열린 외부 페이지가 기존 탭을 피싱 화면으로 바꾸는 과정, noopener를 이용한 방어 방법을 알아봅니다.
웹사이트에서 외부 링크를 누르면 새로운 탭이 열리는 경우가 많다. 사용자는 새 탭에서 내용을 확인한 뒤 이전 탭으로 돌아온다. 문제는 새로 열린 외부 페이지가 기존 탭과 연결된 상태라면 기존 페이지의 주소를 다른 곳으로 변경할 수 있다는 점이다.
공격자는 사용자가 새 탭을 보고 있는 동안 원래 탭을 로그인 화면과 비슷한 피싱 페이지로 바꿀 수 있다. 사용자가 이전 탭으로 돌아와 세션이 만료된 것으로 착각하고 아이디와 비밀번호를 입력하면 계정 정보가 유출될 수 있다. 이러한 공격을 탭내빙 공격이라고 한다.
탭내빙 공격이란?
탭내빙은 브라우저의 탭을 이용해 사용자를 속이는 피싱 공격이다. 사용자가 한동안 확인하지 않은 탭의 내용을 다른 페이지로 변경한 뒤 정상 사이트처럼 위장하는 방식이다.
특히 target="_blank"로 외부 페이지를 새 탭에 열었을 때 새 페이지가 기존 창의 window.opener 객체에 접근할 수 있으면 위험이 커진다. 새로 열린 페이지는 이 객체를 통해 자신을 열어준 원래 탭의 위치를 다른 주소로 변경하려고 시도할 수 있다.
이러한 방식을 일반적인 탭내빙과 구분해 리버스 탭내빙(Reverse Tabnabbing)이라고 부르기도 한다.
사용자는 원래 탭을 직접 열어둔 기억이 있기 때문에 다시 확인할 때 주소창을 자세히 보지 않을 수 있다. 공격자는 이 점을 이용해 기존 서비스와 비슷한 로그인 페이지를 보여준다.
탭내빙 공격의 핵심은 새로 열린 외부 페이지가 사용자가 신뢰하는 기존 탭을 피싱 페이지로 변경한다는 점이다.
target=”_blank”는 어떤 기능일까?
HTML 링크에 target="_blank"를 지정하면 연결된 페이지가 새로운 탭이나 창에서 열린다.
<a href="https://external.example"
target="_blank">
외부 사이트 보기
</a>
사용자가 링크를 누르면 현재 페이지는 그대로 남아 있고 외부 사이트가 새 탭에 표시된다. 여러 정보를 비교하거나 원래 작업 화면을 유지할 수 있어 자주 사용하는 기능이다.
과거의 일부 브라우저 환경에서는 새 페이지가 window.opener를 통해 원래 페이지의 창 객체를 참조할 수 있었다. 서로 출처가 다르면 동일 출처 정책 때문에 원래 페이지의 내용을 마음대로 읽을 수는 없지만, 원래 탭의 주소를 다른 곳으로 이동시키는 동작은 가능할 수 있었다.
최근 주요 브라우저는 target="_blank" 링크에 noopener와 유사한 보호 동작을 기본으로 적용하는 경우가 많다. 하지만 오래된 브라우저, 내장형 웹뷰와 동적으로 창을 여는 코드에서는 동작이 다를 수 있으므로 보안 속성을 명시하는 것이 좋다.
탭내빙 공격이 발생하는 원인
가장 직접적인 원인은 신뢰할 수 없는 외부 링크를 새 창으로 열면서 원래 창과의 연결을 끊지 않는 것이다. 새로 열린 페이지가 공격자에게 넘어가거나 외부 사이트에 스크립트 삽입 취약점이 발생하면 기존 탭이 영향을 받을 수 있다.
사용자가 게시물이나 프로필에 외부 URL을 등록할 수 있는 서비스도 주의해야 한다. 운영자가 모든 외부 페이지의 안전성을 지속적으로 보장할 수 없기 때문이다.
링크를 HTML에 직접 작성하지 않고 JavaScript의 window.open()으로 여는 기능에서도 같은 문제가 발생할 수 있다. 새 창을 연 뒤 opener 참조를 제거하지 않거나 noopener 기능을 적용하지 않으면 원래 창과 연결이 남을 수 있다.
외부 URL의 프로토콜과 도메인을 검사하지 않는 것도 위험하다. 사용자가 입력한 값을 그대로 링크의 href에 넣으면 피싱 사이트나 허용하지 않은 스크립트 형식의 주소가 사용될 가능성이 있다.
탭내빙 공격이 진행되는 원리
공격자는 먼저 외부 링크를 새 창으로 열어주는 웹사이트를 찾는다. 해당 링크에 noopener 보호가 없고 새 페이지에서 window.opener에 접근할 수 있다면 기존 탭의 이동 가능성을 확인한다.
사용자가 정상 사이트에서 링크를 클릭하면 공격자가 관리하는 외부 페이지가 새 탭에 열린다. 사용자의 시선은 새 탭으로 이동하고 원래 탭은 뒤에 남는다.
새 페이지는 일정 시간이 지나거나 사용자가 특정 행동을 했을 때 window.opener를 이용해 원래 탭의 주소를 피싱 페이지로 변경하려고 시도할 수 있다.
개념적으로는 다음과 같은 동작이다.
if (window.opener) {
window.opener.location.replace(
"https://phishing.example/login"
);
}
사용자가 새 페이지를 닫거나 기존 탭을 다시 선택하면 원래 사이트 대신 로그인 화면과 비슷한 피싱 페이지가 표시된다. 사용자는 사이트가 자동으로 로그아웃된 것으로 오해할 수 있다.
실제 공격에서는 피싱 페이지의 디자인, 로고와 문구를 정상 서비스와 비슷하게 구성할 수 있다. 하지만 주소창의 도메인은 원래 사이트와 다르므로 로그인 정보를 입력하기 전 주소를 확인하는 습관이 중요하다.
탭내빙 공격으로 발생할 수 있는 피해
가장 대표적인 피해는 로그인 계정 정보 유출이다. 사용자가 피싱 페이지에 입력한 아이디와 비밀번호는 정상 서버가 아닌 공격자가 관리하는 서버로 전달될 수 있다.
같은 비밀번호를 여러 서비스에서 사용한다면 다른 계정까지 피해가 확대될 가능성이 있다. 회사 계정이 탈취되면 이메일, 내부 문서와 업무 시스템에 접근하는 데 악용될 수 있다.
피싱 페이지에서 결제정보나 개인정보를 다시 입력하도록 유도할 수도 있다. 악성파일 다운로드나 브라우저 알림 권한 허용을 요구하는 화면으로 변경될 가능성도 있다.
탭내빙은 정상 사이트의 서버를 직접 침해하지 않고 사용자의 신뢰와 브라우저 탭 사용 습관을 이용한다. 따라서 서버 로그만으로 피해 여부를 확인하기 어려울 수 있다.
취약할 수 있는 새 창 링크
다음 링크는 외부 사이트를 새 창으로 열지만 원래 창과의 관계를 명시적으로 차단하지 않는다.
<a href="https://external.example"
target="_blank">
외부 사이트 보기
</a>
최신 브라우저에서는 기본적으로 보호될 수 있지만 모든 사용자 환경에서 같은 동작을 보장하기는 어렵다. 특히 오래된 브라우저와 애플리케이션 내부 웹뷰를 지원한다면 rel 속성을 명시하는 것이 안전하다.
게시판에서 사용자가 등록한 외부 링크를 위와 같은 방식으로 출력한다면 외부 페이지의 운영 주체를 신뢰하기도 어렵다. 링크가 생성되는 모든 위치에 일관된 보호 속성을 적용해야 한다.
rel=”noopener noreferrer”를 이용한 방어 방법
새 창으로 외부 링크를 열어야 한다면 rel="noopener"를 함께 지정한다.
<a href="https://external.example"
target="_blank"
rel="noopener">
외부 사이트 보기
</a>
noopener는 새로 열린 페이지에서 원래 창의 window.opener 참조를 사용할 수 없도록 한다. 외부 페이지가 원래 탭의 주소를 피싱 페이지로 변경하는 동작을 차단하는 핵심 설정이다.
외부 사이트에 현재 페이지의 주소 정보를 전달할 필요도 없다면 noreferrer를 함께 사용할 수 있다.
<a href="https://external.example"
target="_blank"
rel="noopener noreferrer">
외부 사이트 보기
</a>
noreferrer는 외부 페이지로 이동할 때 Referer 정보를 보내지 않도록 한다. 많은 브라우저에서 noopener와 유사한 효과도 제공하지만 두 속성의 목적을 명확히 나타내기 위해 함께 작성할 수 있다.
noreferrer를 사용하면 외부 사이트의 방문 분석에서 유입 페이지 정보가 사라질 수 있다. 서비스에서 리퍼러 정보가 필요한지 확인한 뒤 적용하되, 탭내빙 방지를 위한 noopener는 유지해야 한다.
JavaScript로 새 창을 열 때의 대응
JavaScript의 window.open()을 사용할 때도 원래 창과의 관계를 차단해야 한다.
function openExternal(url) {
const newWindow = window.open(
url,
"_blank",
"noopener,noreferrer"
);
if (newWindow) {
newWindow.opener = null;
}
}
브라우저 환경에 따라 기능 문자열 처리 방식이 다를 수 있으므로 noopener를 지정하고 반환된 창의 opener도 null로 설정하는 방식을 사용할 수 있다.
사용자가 입력한 URL을 그대로 window.open()에 전달해서는 안 된다. 새 창 보호와 URL 검증은 서로 다른 문제다. noopener를 적용하더라도 피싱 주소로 직접 이동하는 링크라면 사용자는 여전히 위험한 사이트에 접속하게 된다.
가능하면 외부 링크를 HTML의 <a> 요소로 생성하고 target과 rel 속성을 명확하게 설정하는 것이 관리하기 쉽다.
외부 URL을 안전하게 검증하는 방법
외부 링크를 사용자가 등록할 수 있다면 http와 https처럼 허용할 프로토콜을 제한해야 한다. javascript:와 같이 브라우저에서 코드로 처리될 수 있는 주소 형식은 거부해야 한다.
문자열의 시작 부분만 검사하지 말고 표준 URL 파서를 이용해 프로토콜, 호스트와 포트를 분리해야 한다. 허용된 제휴사 링크만 제공하는 서비스라면 정확한 호스트 목록을 관리하는 것이 좋다.
외부 URL을 화면에 출력할 때 HTML 인코딩만 적용하면 XSS 위험은 줄일 수 있지만 URL 프로토콜까지 안전해지는 것은 아니다. 출력 인코딩과 URL 허용 목록을 함께 적용해야 한다.
링크 옆에 외부 사이트임을 표시하고 실제 도메인을 보여주는 방법도 도움이 된다. 사용자가 클릭하기 전에 이동 목적지를 확인할 수 있기 때문이다.
Cross-Origin-Opener-Policy 적용
웹사이트 전체에서 다른 출처의 창과 opener 관계를 분리하려면 다음 응답 헤더를 검토할 수 있다.
Cross-Origin-Opener-Policy: same-origin
이 설정은 서로 다른 출처의 문서가 같은 브라우징 컨텍스트 그룹을 공유하지 않도록 분리하는 데 도움이 된다.
다만 OAuth 로그인, 외부 결제와 팝업 기반 연동 기능에서는 원래 창과 새 창 사이의 통신이 필요할 수 있다. 설정을 적용하기 전에 정상적인 팝업 흐름에 영향이 없는지 테스트해야 한다.
개별 외부 링크에는 rel="noopener"를 적용하고, 사이트 전체의 격리가 필요한 경우 Cross-Origin-Opener-Policy를 추가로 검토하는 방식이 적절하다.
링크 생성 기능을 공통으로 관리해야 한다
외부 링크를 여러 화면에서 개별적으로 작성하면 일부 링크에 noopener가 빠질 수 있다. 템플릿 컴포넌트나 공통 링크 생성 함수를 만들어 모든 외부 링크에 같은 정책을 적용하는 것이 좋다.
JSP, Thymeleaf와 프론트엔드 프레임워크에서 새 창 링크를 출력하는 부분을 검색해 target="_blank"와 rel 속성을 함께 확인해야 한다.
사용자가 작성한 HTML을 허용하는 게시판이라면 <a> 태그의 target과 rel 속성을 서버 측에서 안전하게 정규화할 필요가 있다. 단순히 위험한 스크립트만 제거하고 새 창 링크 정책을 확인하지 않으면 탭내빙 위험이 남을 수 있다.
외부 링크가 수정되거나 최종 목적지가 다른 도메인으로 바뀌는 경우도 있으므로 중요한 제휴 링크는 주기적으로 확인하는 것이 좋다.
탭내빙 취약점 점검 항목
소스코드 점검에서는 target="_blank"가 사용된 모든 링크를 검색한다. 해당 링크에 rel="noopener" 또는 rel="noopener noreferrer"가 함께 적용돼 있는지 확인해야 한다.
JavaScript 코드에서는 window.open()을 검색하고 외부 입력값이 URL로 전달되는지, noopener 설정과 URL 검증이 적용되는지 살펴본다.
게시판, 댓글, 사용자 프로필과 관리자 화면처럼 사용자가 외부 링크를 등록할 수 있는 기능도 점검 대상이다. 저장된 URL이 화면에 출력될 때 허용된 프로토콜인지 확인하고 탭내빙 방어 속성이 자동으로 추가되는지 살펴봐야 한다.
브라우저 개발자 도구를 이용해 새로 열린 페이지의 window.opener 값이 null인지 확인할 수 있다. 테스트는 본인이 관리하거나 명시적으로 허가받은 시스템에서만 진행해야 한다.
자주 묻는 질문
target=”_blank”를 사용하면 모두 취약한가요?
최신 주요 브라우저는 target="_blank"에 noopener와 유사한 보호를 기본 적용하는 경우가 많다. 하지만 오래된 브라우저와 내장형 웹뷰를 고려해 rel="noopener"를 명시하는 것이 안전하다.
noopener와 noreferrer의 차이는 무엇인가요?
noopener는 새 페이지가 원래 창의 window.opener를 참조하지 못하게 한다. noreferrer는 외부 사이트에 유입 페이지 주소를 전달하지 않으며 브라우저에 따라 opener 관계도 차단한다.
noreferrer를 반드시 사용해야 하나요?
탭내빙 방지의 핵심은 noopener다. 외부 사이트에 Referer 정보를 전달할 필요가 없다면 noreferrer도 함께 사용할 수 있다. 유입 분석이 필요한 서비스에서는 영향을 확인해야 한다.
탭내빙과 오픈 리다이렉트는 같은 공격인가요?
같은 공격은 아니다. 탭내빙은 새로 열린 페이지가 기존 탭을 피싱 화면으로 변경하는 공격이다. 오픈 리다이렉트는 정상 사이트의 이동 기능을 이용해 사용자를 외부 주소로 보내는 취약점이다. 두 문제 모두 피싱에 악용될 수 있다.
주소창을 확인하면 탭내빙 공격을 피할 수 있나요?
피싱 페이지의 도메인은 정상 사이트와 다르므로 주소창 확인이 도움이 된다. 하지만 유사한 도메인과 화면 디자인으로 사용자를 속일 수 있으므로 서비스 제공자는 noopener와 URL 검증을 적용해야 한다.
마무리
탭내빙 공격은 사용자가 새 탭을 확인하는 동안 기존 탭을 피싱 페이지로 변경하는 공격이다. 새로 열린 외부 페이지가 window.opener를 통해 원래 창의 주소를 바꿀 수 있을 때 위험이 발생한다.
새 창으로 외부 링크를 열 때는 target="_blank"와 함께 rel="noopener"를 적용해야 한다. 리퍼러 정보도 숨겨야 한다면 noreferrer를 추가할 수 있다. JavaScript로 창을 여는 기능에서도 opener 관계를 명확하게 차단해야 한다.
탭내빙 방어의 핵심은 새로 열린 외부 페이지와 기존 탭의 연결을 끊는 것이다. noopener, 외부 URL 검증, 공통 링크 컴포넌트와 브라우저 정책을 함께 적용해야 새 창 링크가 피싱 공격에 이용되는 위험을 줄일 수 있다.
핵심 키워드: 탭내빙 공격, Tabnabbing, 리버스 탭내빙, Reverse Tabnabbing, window.opener, target blank, noopener, 새 창 피싱, 탭내빙 방어 방법