“SOP가 못 막는 공격”, CSRF와 XSSI로 알아보는 브라우저의 빈틈
들어가며
지난 글에서는 CORS가 SOP를 우회하기 위해 만들어진 합법적인 통로라는 이야기를 했습니다. 그런데 여기서 한 가지 오해가 생기기 쉽습니다. SOP만 잘 지켜지면 브라우저는 안전하다는 생각입니다. 실제로는 그렇지 않습니다. img 태그, script 태그, form 태그처럼 아주 오래전부터 존재해온 HTML 요소들은 처음부터 SOP의 적용 대상이 아니었습니다. 다른 도메인의 이미지를 가져오고, 다른 도메인의 스크립트를 불러오고, 다른 도메인으로 폼을 제출하는 일은 원래부터 허용되어 있었다는 뜻입니다. 이번 글에서는 바로 이 빈틈을 파고드는 두 가지 공격, CSRF와 XSSI를 다루고, 이를 막기 위해 브라우저가 나중에 추가한 SameSite 쿠키 같은 보완 장치까지 함께 살펴보겠습니다.
SOP는 왜 이 요소들을 처음부터 허용했을까
SOP가 처음 만들어진 시점을 생각해보면 이해가 쉽습니다. 웹이 지금처럼 복잡한 애플리케이션을 돌리는 플랫폼이 되기 전, 웹 페이지는 서로 다른 서버의 이미지를 가져와 붙이고, 외부 스크립트 라이브러리를 불러오고, 다른 도메인에 있는 검색엔진이나 결제 서비스로 폼을 제출하는 정도의 상호작용이 기본이었습니다. 이런 동작을 전부 막아버리면 웹이라는 생태계 자체가 성립하지 않았을 겁니다. 그래서 브라우저는 리소스를 불러오는 행위와 다른 출처의 데이터를 읽어서 자바스크립트로 조작하는 행위를 구분했습니다. 이미지를 화면에 그리거나 스크립트를 실행하는 것은 허용하되, 그 결과로 얻은 내용을 다른 출처의 스크립트가 직접 읽어들이는 것은 막는 방식입니다. 문제는 이 구분이 완벽하지 않다는 데 있습니다. 읽지는 못해도 실행은 되기 때문에, 실행되는 과정 자체를 관찰하거나 실행의 부수 효과를 이용하면 정보를 빼낼 수 있는 틈이 생깁니다. CSRF와 XSSI는 각각 다른 방식으로 이 틈을 파고듭니다.
CSRF: 세션 쿠키가 자동으로 실리는 특징을 노린 공격
CSRF는 Cross Site Request Forgery의 줄임말로, 공격자가 사용자를 속여서 사용자도 모르는 사이에 특정 사이트로 요청을 보내게 만드는 공격입니다. 원리는 생각보다 단순합니다. 사용자가 은행 사이트에 로그인해서 세션 쿠키를 가지고 있는 상태라고 가정해봅시다. 이 상태에서 공격자가 만든 페이지를 열었는데, 그 페이지 안에 숨겨진 form 태그가 있고 이 폼의 action이 은행 사이트의 계좌 이체 API를 가리키고 있다면 어떻게 될까요. 사용자가 페이지를 여는 순간 자바스크립트로 이 폼이 자동 제출되도록 만들어두면, 브라우저는 해당 요청을 은행 사이트로 보내면서 그 도메인에 저장되어 있던 세션 쿠키를 자동으로 함께 실어 보냅니다. 브라우저 입장에서는 쿠키를 누가 요청을 트리거했는지가 아니라 어떤 도메인으로 요청이 가는지만 보고 자동으로 붙여주기 때문에 벌어지는 일입니다. 은행 서버는 유효한 세션 쿠키가 붙어 있으니 정상적인 사용자의 요청이라고 판단하고 이체를 처리해버립니다. 사용자는 자신이 방문한 페이지가 그런 요청을 보냈는지조차 알지 못합니다.

SameSite 쿠키 속성이 CSRF를 막는 방식
이 공격이 오랫동안 골칫거리였던 이유는 쿠키의 자동 전송이라는 브라우저의 기본 동작 자체를 문제 삼아야 했기 때문입니다. 그래서 나온 해법이 쿠키에 SameSite라는 속성을 붙이는 것입니다. 이 속성을 Strict로 설정하면 쿠키가 발급된 사이트에서 직접 발생한 요청에만 쿠키가 실려 나가고, 외부 사이트에서 유발된 요청에는 아예 쿠키가 붙지 않습니다. 앞서 설명한 은행 이체 시나리오라면, 공격자의 페이지에서 시작된 요청이므로 쿠키가 실리지 않아 이체 요청 자체가 인증되지 않은 요청으로 거부됩니다. 다만 Strict는 보안은 강력하지만 사용성 문제를 만들 수 있습니다. 예를 들어 다른 사이트에 있는 링크를 눌러서 우리 서비스로 들어오는 경우에도 쿠키가 빠져서 로그인이 풀린 것처럼 보일 수 있습니다. 그래서 실무에서는 절충안인 Lax를 많이 사용합니다. Lax는 사용자가 직접 클릭해서 이동하는 것 같은 안전한 탐색 요청에는 쿠키를 붙이고, 폼 자동 제출이나 이미지 태그로 유발되는 요청처럼 사용자의 의도가 개입하지 않은 요청에는 쿠키를 붙이지 않는 절충안입니다. CSRF 토큰을 별도로 발급해서 요청마다 검증하는 전통적인 방식과 SameSite 속성을 함께 쓰면 훨씬 촘촘한 방어가 가능합니다.
XSSI: 스크립트 태그로 교차 출처 데이터를 훔치는 기법
XSSI는 Cross Site Script Inclusion의 줄임말로, script 태그가 어떤 도메인의 자바스크립트 파일이든 가져와서 실행할 수 있다는 점을 악용하는 공격입니다. 문제는 자바스크립트 파일이 아닌데도 자바스크립트처럼 실행되도록 서버가 데이터를 내려주는 경우입니다. 예전에 일부 API 서버들은 로그인한 사용자의 데이터를 JSON이 아니라 자바스크립트 변수 할당문 형태로 내려주곤 했습니다. 이런 응답을 공격자가 자신의 페이지에서 script 태그로 그대로 불러오면, 그 변수가 공격자 페이지의 전역 스코프에 그대로 생성됩니다. 공격자는 자신의 페이지에 미리 정의해둔 변수 이름을 이용해서 그 값을 읽어가기만 하면 됩니다. 브라우저는 이 스크립트가 어느 도메인에서 왔는지는 신경 쓰지 않고 그저 실행 가능한 코드로 실행할 뿐이라, SOP가 전혀 개입하지 못하는 지점입니다. 이 공격을 막기 위해 등장한 방어법이 응답 앞에 while(1) 이나 for(;;) 같은 무한 루프 구문을 붙이는 방식입니다. 정상적인 자바스크립트 실행 환경에서 애플리케이션이 데이터를 파싱할 때는 이 접두어를 미리 제거하고 처리하도록 만들어두고, 공격자가 script 태그로 그대로 불러오면 무한 루프에 걸려 실행이 멈추게 만드는 구조입니다. 지금은 대부분의 API가 JSON으로 응답하고, Content-Type을 application/json으로 명확히 지정하면 브라우저가 이를 실행 가능한 스크립트로 취급하지 않기 때문에 예전만큼 흔한 공격은 아니지만, 오래된 레거시 API나 서버사이드 렌더링 페이지에 초기 데이터를 스크립트 변수로 심어두는 패턴이 남아 있다면 여전히 점검해볼 가치가 있습니다.

SOP의 예외 리소스를 정리하면
정리하면 SOP는 이미지, 스크립트, 스타일시트, 폰트, 비디오 같은 리소스를 다른 출처에서 불러와 브라우저 화면에 표시하거나 실행하는 행위 자체는 막지 않습니다. 막는 것은 그 결과물의 내용을 자바스크립트가 직접 읽어들이는 행위입니다. 이미지의 픽셀 데이터를 캔버스에 그린 뒤 getImageData로 읽으려 하면 교차 출처 이미지의 경우 오염된 캔버스로 처리되어 읽기가 차단되고, 스타일시트도 마찬가지로 cssRules 같은 속성에 접근하려 하면 제한이 걸립니다. 하지만 스크립트만큼은 애초에 읽는 것이 아니라 실행하는 것이 목적이라 이런 차단 로직을 적용하기가 까다롭고, 그 결과가 XSSI 같은 공격으로 이어졌습니다. form 태그를 통한 제출 역시 결과 페이지의 내용을 읽어오는 것은 막히지만 요청 자체가 나가는 것은 막히지 않아서 CSRF의 통로가 됩니다.
마무리
SOP는 완벽한 방패가 아니라 브라우저가 오랜 시간에 걸쳐 필요할 때마다 예외를 인정하고 보완해온 규칙의 모음에 가깝습니다. CSRF와 XSSI는 그 예외 조항을 정확히 알고 있던 공격자들이 찾아낸 틈이었고, SameSite 쿠키와 Content-Type 검증 같은 보완책이 하나씩 추가되면서 지금의 형태가 되었습니다. 최근에는 여기서 한 걸음 더 나아가 CSP로 어떤 출처의 스크립트만 실행을 허용할지 명시적으로 선언하는 방식까지 쓰이고 있습니다. 결국 브라우저 보안은 하나의 정책이 아니라 SOP, CORS, SameSite, CSP가 각자의 역할을 나눠 맡으며 겹겹이 쌓인 구조라는 점을 이해하는 것이 실무에서 가장 중요한 감각이라고 생각합니다.
참고
이 글은 SOP, CSRF, XSSI와 관련된 공개된 웹 표준 문서 및 개발 자료를 바탕으로 이해를 돕기 위해 정리한 내용입니다.
홍TV



![Eclipse에서 갑자기 "[m2e] Lifecycle Mapping" 오류? Maven 프로젝트가 빨간 줄 뜨는 이유 6 Eclipse에서 갑자기 [m2e] Lifecycle Mapping 오류? Maven 프로젝트가 빨간 줄 뜨는 진짜 이유](https://hongtv.co.kr/wp-content/uploads/2026/09/62edeb0f-aaf8-42cc-af31-1a91e54bc187-300x200.png)
댓글 0
첫 댓글을 남겨보세요.