주제 : iframe, http header 이해과정의 서술 (보안에 대해 쬐금 알게 된것도 ㅎㅎ)
목적: 힘들게 알아보았던 과정을 다시 잊어버리지 않기위한 복기 및 다른 사람들에게 알리는 것
현재 진행하고 있는 프로젝트의 가장 핵심이라고 볼 수 있는 기능을 구현함에 있어 어려움에 부딪혔다.
그 기능은 바로 iframe으로 띄운 다른 웹사이트의 내용을 현재 페이지에 그대로 드래그 할 수 있게 해주는 것이었다.
기능을 구현하기 위해 공부 했던 것들과 그로 인한 결론을 차례대로 서술해가기로 했다.
1. iframe 의 역사 (제약 사항이 나타나게 된 것을 위주로)
-XSS 로 인한 제약사항 발생
2. iframe 제약사항 극복하기 위한 방법
-JSONP
-PROXY
3. 보안 문제를 우회하는 해결책.
1. iframe 의 역사
iframe의 기능은 현재 페이지에 또다른 페이지를 포함하도록 해준다. 비슷한 분야에서 embed 태그라던가 object 태그가 있지만 iframe이 가장 많이 쓰이지 않나 조심히 추측해본다(실제로 iframe을 위주로 보아 왔다. 특히 Youtube가 iframe으로 지원하기때문에...)
iframe은 inline frame의 약자로써, 바로바로 쓸 수 있는 프레임이라는 뉘앙스를 가지고 있다. 과거 html 4.01 명세서에는 frame 태그가 존재했었다. 이 frame 태그는 body 태그 대신 frameset 태그와 같이 쓰이면서 페이지의 구조를 잡고 분할 하는 것을 쉽게 해주었었다. 하지만 html 5에 와서는 obsolete 되어버리는데 일단 로딩하는데 많은 시간을 잡아먹을 뿐더러, 동작, 보안 등의 분야에서 많은 문제점이 발견되었기 때문이다.
{
태그의 역사에 대해서는 잘 정리한 다음 사이트를 참고하시기를
http://www.martinrinehart.com/frontend-engineering/engineers/html/html-tag-history.html
}
iframe은 frame 태그와 마찬가지로 html4.01 명세서에서 등장하였다. 페이지 내부에 다른 페이지를 독립적으로 띄울 수 있다는 것은 충분히 매력적이었다. PC에서 도스같이 커맨드를 일일이 쳐야 하는 CLI 대신에 윈도우 처럼 화면을 마우스로 통제할수 있는 GUI 가 멀티태스킹을 가능하게 해주었듯이 iframe 같은 기능은 브라우저 내에서 멀티태스킹을 원활하게 해주었기 때문이다. 즉, 많은 정보를 한 페이지에 담아 낼 수 있으므로 비용면에서 훨씬 경제적이었다는 것이다.
훌륭한 기능을 가지고 있음에 많은 사람들이 열광하였지만 인기에 비례하여 악성 사용자도 나타나기 시작했다. 생각을 해보자 현재 사이트에 다른 사이트를 띄울 수 있다는 것은 악의를 가지고 만든 사이트 역시 띄울 수 있다는 것을 의미한다.
- XSS (Cross Site Scripting) 공격에 대한 방어
iframe은 현재페이지 말고도 다른 페이지를 띄울 수 있는 기능이기에 XSS공격이 가능했다.
가령 어떤 게시판이 있다고 생각해보자. 이 게시판의 게시글에 iframe이 올라갈 수 있다면
악의적인 스크립트를 가지고 있는 사이트를 띄워 놓을 수 있을 것이다. 그러다가 다른 사용자가 이 게시물에 접근하면 iframe을 통해 보여지고 있는 악의적인 스크립트에 그대로 노출된다 (사용자의 쿠키정보 등등을 탈취당하는 것이다).
자바스크립트에 대한 SOP( Same Oringin Policy) 정책덕분에 이는 간단히 해결되었다. 같은 도메인(같은 호스트와 포트를 지니고 있어야)하기 때문에 같은 서버의 페이지 말고는 frame형식으로 접근을 할 수 없기때문이다. 같은 도메인이면 다른 외부로부터의 공격이 아니라고 판단할 수 있다.
결국 악의적인 사용자들 덕분에 선량한 개발자들은 피해를 보게되었다. 앞서 이야기 했듯이 iframe으로 인해 외부의 여러 페이지를 띄움으로써 효율적인 개발을 할 수 있었는데, SOP덕분에 그 길이 막혀버린 것이다.
이와 더불어 CSP(Content Security Policy)라는 것도 생겨났는데, 이는 서버쪽에서 자신의 콘텐츠가 무작위로 프레임에 불려나가는 것을 막을 수 있게 해주었다. 악의적 프로그래머들이 유명한 페이지들을 자신의 프레임에 가져와서 ClickJacking 같은 방법을 써먹곤 했기 때문이다.
이래나 저래나 iframe을 다루는 개발자들에게는 제약이 된것이 사실이다.
2. iframe 제약사항 극복하기 위한 방법
위에서 말한 보안 제약사항이 내가 프로젝트를 진행함에 있어 기능을 구현하는데 큰 제약이 되었다.
나는 다른 외부의 사이트들을 한 페이지에 표현하고 싶었다.
-CORS
선량한 개발자들의 숨통을 틔워주고자 CORS가 나타났다. Cross Origin Resource Sharing의 약자로 도메인이 다른 서버로부터 자원을 받을 수 있도록 허용해주는 방법이 생겨났다. 비교적 최신 브라우저에서 도입이 되었다.
CORS는 서로 약속된 규정을 지킨 브라우저와 서버간에만 허용하도록 해주었기때문에 보안적으로도 안전했다. iframe을 사용하는 개발자는 자신이 직접 다룰수 있는 서버만을 통해 자원을 주고받을 수 있는 점이 중요하다.
나는 특정 서버만을 보여주는 것이 아니라 불특정 다수서버를 전부 보여주고 싶었기때문에 이는 내게 해결방법이 되지 않았다.
계속해서 방법을 찾아나섰다. 어떻게 해야 다른 웹사이트를 에러없이 안전하게 iframe으로 표시를 해줄 수 있을까?
결국 손을 대기 시작한것이 http header 였다. 위에서 보여지는 많은 제약사항들은 header를 조작함으로써 생겨나기 때문이었다.
HTTP 완벽가이드
다람쥐가 꽤나 귀여워서 친근감이 갔다 ^^
2002년에 써진 책인데, 한글 번역은 2014년에 이뤄졌다. 최신 트렌드도 담고 있어 전혀 아웃데이트된 책은 아니었다. 그리고 번역하시는 분들이 신경을 많이써서 읽기도 편했다.
이 책을 읽으면서 http 통신에 대한 조그마한 지식을 쌓았다. 이를 바탕으로 몇가지 시도를 해보았다.
먼저 가장 근본적인 제약사항인 SOP를 깨기위한 방법을 찾아 나섰다. 그 첫번째로 JSONP을 이용한 방법이다.
-JSONP
AJAX 기법을 통해 다른 사이트로부터 HTML 페이지 정보를 가져와 iframe에 넣는 것을 생각해내었다.
하지만 AJAX 역시 SOP로부터 자유로울 수 없었는데, 이를 타개할 방법이 바로 JSONP 기법이었다.
자세하게 설명은 아래 주소로부터 얻으시길 바란다.
http://dev.epiloum.net/1311
자바스크립트 예제도 잘 설명되어 있어 이해하는데 어렵지 않았다. JSONP 기법은 GET method 만 가능했지만 애초에 나는 GET만 있어도 충분했기 때문에 별 신경쓰지 않았다.
그러나 생각처럼 되지를 않았다...
Angular2 를 쓰고 있던 나에게 떨어진 에러는 다음과 같았다
Response with status: 200 Ok for URL
????? 이게 무슨 소리지??? 응답받은 상태코드는 200 ok 였다. 분명 잘 통신을 했다는 것인데 읽을 수가 없다고 크롬 개발자 도구가 알려주었다. 정신이 혼미해지면서 이유를 찾아 나섰다.
이유는 간단했다.
JSONP은 콜백을 준비한 상태에서 리퀘스트를 요청했어야 했다. 무슨 말이고 하니
http://www.google.com 과 같이 보내는 게 아니라 쿼리스트링에 callback을 추가시켜줘야 했다. 에러메세지에서도 알려주듯이 프로미스가 제대로 해결을 할 수 없었던 것은 callback 함수가 준비되지 않았기 때문이었다.
http://www.google.com?callback=JSONP_CALLBACK 으로 보내주어야 JSONP이 제대로 작동을 했다.
그럼 해결될줄 알았더니 이번엔 다음과 같은 에러를 뱉어냈다.
SyntaxError: Unexpected token <
이건 또 무슨소리인가...? '<' 이게 뭐 어쨌다고!!! 라고 생각하고 다시 구글링에 들어갔다.
하지만 이에 대한 해결책을 알게 된것은 다른 방법을 모색하던 중에 깨닫게 되었으니... 다음 방법에서 자세히 설명하도록 하겠다.
어쨌든 JSONP 으로는 iframe에 웹사이트를 표시하는 것이 불가능했다...
-PROXY
프록시는 쉽게 말하면 중개서버이다. 프록시에 대해 자세히 알고 싶다면 위에서 설명했던 HTTP 완벽가이드를 읽어봤으면 한다.
iframe에 들어가는 것은 어차피 html 문서라는 점과 웹 크롤링이라는 기술을 착안해서 생각해냈다. (페이스북에 질문을 올려서 프록시라는 방법을 알게되었을때 머릿속에 떠올랐다)
웹크롤링(정확한 명칭은 스크래핑이라는 글도 있다.)을 진행했던 소스코드가 있었다. html을 분석하는 그 코드를 보고 있으니 html 문서 전체를 가져 올 수도 있겠다고 생각했다.
먼저 JAVA Spring 으로 간단한 컨트롤러를 작성했다.
급조한 코드라 조잡하지만 요지는 다음과 같다.
httpClient 객체를 통해 httpResponse를 얻어낸다. 그 안에 있는 엔터티를 조사해
서버가 html 문서를 스트링 형태로 지니고 있고, 이 중개 서버에 브라우저가 접속해서 정보를 빼오는 방식이었다. 생각처럼만 된다면 문제 될 것이 없었다. 크롬 브라우저를 통해 컨트롤러에 요청을 보내보았다
윗 사진이 스프링 부트로 만들어진 서버 localhost:8080/iframe 에 접속한 상태이다.
아래쪽은 정상적으로 google.com 에 접속한 상태이다.
보시다시피 화면이 와장창 깨져서 날라왔다.
이유는 간단했다. 구글의 이미지같은 파일의 경로가 문제였다.
/image/google.png 같은 경로였다. 즉 상대경로를 그대로를 받아온 중개서버가 다시 브라우저로 보내왔을때는 도메인이 localhost:8080 이었기 때문에,
http://localhost:8080/image/google.com 같이 이미지를 불려오려고 했기때문에 화면이 깨졌던 것이다. 내가 급조한 서버에는 저런 이미지는 존재하지 않았다.
그럼 서버에서 브라우저처럼 모든 이미지와 텍스트를 보유한다음에 보내주면 잘 보여주지 않을까 생각도 해보았다. 하지만 내가 진행하고 있는 이 프로젝트는 돈을 받고 하는 것이 아니었다.
서버의 성능이 좋고, 내게 돈이 있다면 충분히 그렇게 구축해 볼 수 있을 것이다. 사용자가 얼마나 될지는 모르겠지만 모든 사이트의 이미지와 그외 assets 파일들을 감당할 수 있을지에 고려해본다면 얼마나 멍청한 짓인지 금방 느껴볼 수 있다.
캐시 처럼 계속 보관하는게 아니라 일정 시간만 보관하고 다른 사용자들이 같은 것들을 필요로 할때 보여주게 할 수도 있겠지만 현재 상태로는 너무 벅찼다. 서버쪽을 담당하고 있는 형님께 건의한번 해봐야 겠다!
글을 쓰다보니 생각남.
깨져있는 상태여도 일단 만들고 있는 페이지에 어떻게 붙는지, 확인해보고 싶었다.
같은 호스트네임이여도(localhost) 포트번호가 다르면 다른 도메인으로 인식하기 때문에,
프론트 서버와 컨트롤러 서버간에 통신을 위해서 JSONP을 도입했다. 참 멍청했던게 그냥 프론트 서버에 html 문서로 불러와도 되는 거였는데 계속 문제에 매달리다보니 판단이 흐려졌던 것 같다.
덕분에 아까 이야기했던 SyntaxError: Unexpected token < 에러에 대한 원인을 알수 있었다.
프록시 서버를 개설해서 받아오는 과정에서 도메인이 다르다는 이유로 JSONP을 쓴다고 이야기를 했었다. 어떻게 나타나는지 직접 확인해보도록 하자. 위에서 보여주었던 컨트롤러에서 리턴부분을 잘 보자. 객체.name 으로 나타나는데 처음에는 아무생각없이 이런식으로 스트링으로 리턴값을 줬었다.
바로 스트링으로 송출하는데서 에러가 나타나게 되었던 것이다.
JSONP은 말그대로 JSON padding 방식이다. 응답되는 데이터가 JSON 형식이 아니면 SyntaxError: Unexpected token < 가 날 수밖에 없다는 소리다.
callback(<html>) 형식인데, 저 함수를 어떻게 읽어 낼수 있을까? JSONP을 쓰기 위해서 JSON 형태로 만들어줄 필요가 있었고, 나는 객체로 반환하도록 컨트롤러를 조정했다.
객체로 조정하고 나서 요청에 따른 화면이다.
어떤 차이가 보이시는지? 요청된 URL에 따라 보여지는 객체 모습이 다르다. 객체를 JSON으로 반환해서 보내주는데, 처음에 보여주는 URL은 그냥 {} 형태이다. 두번째 제대로된 JSONP 요청 형태에서는
콜백함수에 감싸진 형태이다. 콜백에 대한 쿼리스트링이 없는 URL로 보내면 JSONP 자체가 만들어지지 않으므로 크로스도메인 문제를 해결할 수 없고, 콜백 쿼리스트링을 넣어주더라도 그 반환되는 객체가 JSON 객체가 아닌 단순 html 문서이면 'SyntaxError: Unexpected token <' 에러가 난다는 이야기이다!
다음 포스트에서는 어떻게 프로젝트에 합당하는 방법을 찾았는지에 대한 해결책을 제시한다.
댓글
댓글 쓰기