에디터에 링크 하나 넣는 거, 별거 아닌 것 같죠? 저도 그렇게 생각했어요. a 태그 붙이면 끝인 줄 알았거든요. 그런데 새 탭으로 열었다가 보안 구멍을 냈고, 사용자가 넣은 주소를 그대로 믿었다가 또 위험할 뻔했어요. 유튜브 영상을 본문에 바로 재생되게 넣는 임베드(embed, 외부 콘텐츠 삽입)는 더 조심스러웠고요. 오늘은 링크와 임베드를 안전하게 다루는 법을, 상황별로 하나씩 풀어볼게요.
22.1 링크는 그냥 a 태그면 되잖아요?
맞아요, 링크의 기본은 a 태그예요. 사용자가 주소와 보일 글자를 정하면 이렇게 만들죠.
<a href="https://example.com">예제 사이트</a>
여기까지는 누구나 해요. 문제는 이 링크를 누르면 어떻게 열리느냐예요. 기본은 지금 보던 페이지에서 그 주소로 이동해요. 그런데 사용자가 글을 읽다 링크를 눌렀을 때 우리 사이트를 떠나버리면 아쉽죠. 그래서 흔히 새 탭으로 열고 싶어 해요. 여기서부터 조심할 게 생겨요. 새 탭 열기가 편하기만 한 게 아니거든요.
22.2 새 탭으로 열면 왜 위험해요?
새 탭은 target="_blank"로 열어요. 그런데 이것만 쓰면 탭내빙(tabnabbing)이라는 공격에 노출돼요. 새로 열린 그 페이지가 우리를 열어준 원래 탭을 조종할 수 있거든요. 몰래 가짜 로그인 페이지로 바꿔치기하는 식이에요. 사용자는 원래 탭으로 돌아왔다가 진짜인 줄 알고 비밀번호를 넣죠.
<a href="https://example.com" target="_blank"
rel="noopener noreferrer">예제 사이트</a>
막는 방법은 rel="noopener noreferrer" 한 줄이에요. noopener는 새로 열린 페이지가 원래 탭을 건드리지 못하게 끊어줘요. noreferrer는 어디서 왔는지 정보까지 안 넘기고요. 그래서 target="_blank"를 쓸 땐 rel도 항상 세트로 붙여야 해요. 요즘 브라우저는 기본으로 막아주기도 하지만, 우리가 직접 붙여두는 게 안전해요. 에디터가 링크를 만들 때 자동으로 이 rel을 넣어주게 짜두면 실수할 일이 없어요.
22.3 사용자가 넣은 링크를 그대로 믿어도 돼요?
절대 아니에요. 앞 편에서 본 javascript: 스킴 기억나죠? 사용자가 링크 주소랍시고 이런 걸 넣을 수 있어요.
<a href="javascript:steal()">경품 받기</a>
겉보기엔 평범한 링크인데, 누르면 공격 코드가 실행돼요. 그래서 링크 주소는 넣기 전에 검사해야 해요. 방법은 간단해요. 안전한 스킴만 허용하는 거예요.
function safeUrl(url) {
if (/^https?:\/\//i.test(url)) return url;
return "#";
}
http나 https로 시작하는 주소만 통과시키고, 나머지는 무해한 값으로 바꿔요. 이러면 javascript:나 data: 같은 위험한 스킴은 애초에 링크가 안 돼요. 여기서도 원칙은 같아요. 위험한 걸 골라 막는 게 아니라 안전한 것만 허용하는 화이트리스트 방식이요. 이 검사는 sanitize 라이브러리가 대부분 해주지만, 링크를 직접 만든다면 우리도 한 번 더 확인하는 게 좋아요.
22.4 유튜브 영상을 본문에 넣고 싶어요
링크만으로는 아쉬울 때가 있어요. 유튜브 주소를 넣으면 영상이 본문에서 바로 재생되면 좋겠죠. 이걸 임베드라고 해요. 외부 콘텐츠를 iframe(다른 페이지를 창처럼 품는 태그)으로 끼워 넣는 거예요.
<iframe src="https://www.youtube.com/embed/VIDEO_ID"
allowfullscreen></iframe>
유튜브가 제공하는 embed 주소를 iframe의 src에 넣으면, 그 자리에 플레이어가 떠요. 여기서 흔히 막히는 게 있어요. 사용자는 보통 watch가 붙은 일반 주소를 복사해 오거든요. 그건 iframe에 바로 못 넣어요. 그래서 우리가 그 주소에서 영상 아이디를 뽑아 embed 주소로 바꿔 줘야 해요. 이 변환을 에디터가 자동으로 해주면 사용자는 그냥 주소만 붙이면 되고요.
변환할 때 영상 아이디 부분만 뽑아 쓰는 것도 잊지 마세요. 유튜브 주소에는 재생 목록이나 시작 시간 같은 덤 정보가 붙어 오는 경우가 많은데, 그걸 검사 없이 그대로 embed 주소에 이어 붙이면 엉뚱한 값이 섞여 들어갈 수 있어요. 필요한 아이디만 정확히 골라내고 나머지는 버리는 게 깔끔하고 안전해요.
22.5 oEmbed가 뭐예요?
유튜브야 규칙을 알면 되지만, 트위터, 인스타그램, 지도처럼 서비스마다 임베드 방식이 다르면 하나하나 대응하기 벅차요. 이때 쓰는 표준이 oEmbed예요. "이 주소를 임베드하려면 어떤 HTML을 써야 해?"라고 서비스에 물어보는 약속된 방법이에요.
흐름은 이래요. 사용자가 링크를 넣으면, 우리 서버가 그 서비스의 oEmbed 엔드포인트(정해진 문의 주소)에 원래 링크를 넘겨 물어봐요. 그러면 서비스가 바로 쓸 수 있는 임베드 HTML을 JSON에 담아 돌려주죠. 우리는 그 HTML을 본문에 넣기만 하면 돼요. 서비스마다 다른 규칙을 우리가 외울 필요 없이, 표준 질문 하나로 해결되는 게 장점이에요. 다만 돌려받은 HTML도 그대로 믿으면 안 되고, 다음에 볼 검사를 거쳐야 해요.
22.6 아무 iframe이나 넣어도 되나요?
여기가 임베드의 진짜 위험 지점이에요. iframe은 남의 페이지를 통째로 우리 화면에 품는 태그라, 아무 주소나 넣게 두면 악성 사이트를 심을 수 있어요. 그래서 두 가지를 걸어요. 첫째, 믿는 도메인만 허용해요.
const OK = ["youtube.com", "player.vimeo.com"];
function allowEmbed(src) {
const host = new URL(src).hostname;
return OK.some((d) => host.endsWith(d));
}
유튜브, 비메오처럼 우리가 정한 도메인에서 온 iframe만 통과시키고 나머지는 버려요. 둘째, iframe에 sandbox 속성을 걸어 품은 페이지가 할 수 있는 일을 제한해요. 이렇게 도메인 화이트리스트와 sandbox를 함께 쓰면, 편리한 임베드를 안전하게 누릴 수 있어요. 임베드는 힘이 센 만큼 문을 좁게 여는 게 핵심이에요.
22.7 오늘 정리
링크와 임베드는 작아 보여도 보안이 걸린 영역이었어요. 새 탭은 target="_blank"에 rel="noopener noreferrer"를 세트로 붙여 탭내빙을 막았고, 사용자가 넣은 주소는 http와 https만 허용하는 화이트리스트로 걸러 javascript: 링크를 차단했죠. 유튜브 임베드는 일반 주소를 embed 주소로 변환해 iframe에 넣었고, 여러 서비스를 아우르려면 oEmbed라는 표준 질문을 썼어요. 무엇보다 iframe은 도메인 화이트리스트와 sandbox로 문을 좁게 열어야 안전했고요. 링크든 임베드든 원칙은 하나였어요. 안전한 것만 허용하는 거예요.