URL의 # 뒤 내용은 왜 서버로 전송되지 않을까
인터넷 주소 가운데 물음표나 샤프(#) 뒤에 붙은 긴 문자열을 본 적이 있으실 겁니다. 이 중 샤프(#) 뒤의 부분은 다른 부분과 달리 서버로 전송되지 않는다는 특별한 성질이 있습니다. 친구와 암호 대화는 이 성질을 이용해 해독 키를 서버에 보내지 않고도 번역기를 공유할 수 있게 만들었습니다. 이 글에서는 그 원리와 장점, 그리고 반드시 알아 두셔야 할 주의점을 차례로 설명합니다.
주소의 구조부터 가볍게 짚어 보기
웹 주소는 여러 조각으로 이루어져 있습니다. 예를 들어 https://example.com/room/abc?page=2#memo 라는 주소가 있다고 해 봅시다. https 는 통신 방식, example.com 은 접속할 서버의 이름, /room/abc 는 서버 안의 경로, ?page=2 는 서버에 전달하는 추가 조건입니다. 그리고 맨 끝의 #memo 가 바로 URL 조각, 영어로는 프래그먼트(fragment)라고 부르는 부분입니다.
이 조각은 원래 책의 책갈피와 같은 용도로 만들어졌습니다. 긴 문서에서 특정 소제목 위치로 바로 이동하고 싶을 때 쓰는 표시입니다. 문서 자체는 서버에서 통째로 받아 오고, 그중 어디를 화면에 보여 줄지는 브라우저가 혼자 결정합니다. 그래서 서버는 조각 값을 알 필요가 없습니다.
왜 서버로 전송되지 않을까
브라우저가 주소를 열 때 서버에 보내는 요청에는 어느 서버의 어느 경로를 원하는지만 들어갑니다. 위 예시라면 요청 줄에는 /room/abc?page=2 까지만 실리고 #memo 는 빠집니다. 조각은 서버가 아니라 브라우저 안에서만 쓰이는 값이라는 것이 웹 표준의 약속입니다.
이 점은 서버 쪽에서 아무리 로그를 꼼꼼히 남겨도 마찬가지입니다. 접속 기록, 오류 기록, 중간에 있는 프록시나 로드 밸런서의 기록에는 요청 줄에 있던 내용만 남기 때문에, 애초에 전송되지 않은 조각은 기록할 수도 없습니다. 이 서비스는 이 점을 믿고 설계되었습니다.
이 서비스는 조각을 어떻게 쓰나요
번역기를 만들면 브라우저가 무작위로 32바이트짜리 번역기 키를 만듭니다. 번역기 주소는 /secretmessage/<번역기 id>#<번역기 키> 모양이 되는데, 키는 # 뒤의 조각에만 들어갑니다. 서버가 아는 것은 번역기 id 뿐이고 키는 모릅니다. 서버 코드도 주소 키, 비밀번호, 평문을 받지 않는다고 스스로 밝히고 있으며, 실제로 번역기를 만들 때 서버가 받는 값은 키 자체가 아니라 키에서 따로 계산한 관리 토큰 등입니다.
글을 암호화하고 푸는 일은 모두 브라우저 안에서 일어납니다. 비밀번호가 없는 번역기라면 해독 키는 번역기 키에서 HKDF라는 방식으로 바로 만들어집니다. 따라서 서버가 침해되어 저장된 데이터가 모두 유출된다고 해도, 거기에는 해독 키가 들어 있지 않습니다. 참고로 번역기 id는 무작위 16바이트로 만들어지지만, 주소를 아는 사람은 누구나 쓸 수 있다는 점은 변하지 않으므로 필요하면 비밀번호를 함께 거는 것이 좋습니다. 이 구조의 더 넓은 배경은 서버가 내용을 모르게 만드는 설계 글에서 이어서 다룹니다.
Referer 헤더에는 실리지 않을까요
링크를 눌러 다른 사이트로 이동하면 브라우저가 어디서 왔는지를 알려 주는 Referer 헤더를 보낼 수 있습니다. 이때 브라우저는 주소 중 # 조각 부분을 빼고 보내도록 되어 있습니다. 그래서 일반적인 상황에서는 조각이 이 경로로 새어 나가지 않습니다.
이 서비스는 여기서 한 걸음 더 나아가 번역기 화면을 보낼 때 Referrer-Policy: no-referrer 헤더를 붙입니다. 이 설정은 아예 출처 정보를 보내지 말라는 뜻입니다. 또 콘텐츠 보안 정책(CSP)으로 스크립트와 연결 대상을 이 사이트 자신으로 제한하고, 외부 스크립트나 인라인 스크립트를 두지 않습니다. 주소 키가 조각에 있는 만큼, 제3자의 코드가 끼어들 여지를 줄이려는 조치입니다.
그래도 조각이 드러나는 경우들
조각이 서버로 가지 않는다는 말은 어디에도 남지 않는다는 뜻이 아닙니다. 주소는 결국 사람과 기기가 다루는 문자열이므로 다음과 같은 경로에서 노출될 수 있습니다.
- 브라우저 방문 기록: 많은 브라우저는 # 조각까지 포함해 방문한 주소를 저장합니다. 공용 컴퓨터에서 열었다면 기록이 남을 수 있습니다.
- 주소 복사와 공유: 주소창의 주소를 그대로 복사해 메신저나 메모, 이메일에 붙이면 키도 함께 갑니다. 번역기 주소를 보낸 대화방이 곧 키가 놓인 곳이 됩니다.
- 브라우저 확장 프로그램: 확장이 열린 탭의 주소를 읽을 권한을 가지고 있다면 조각까지 볼 수 있습니다. 믿을 수 없는 확장은 설치하지 않는 편이 낫습니다.
- 동기화와 백업: 브라우저 계정 동기화가 켜져 있으면 방문 기록이 다른 기기나 클라우드로 옮겨질 수 있습니다.
- 화면 노출: 주소창이 화면 공유나 어깨너머로 보일 수 있습니다.
정리하면, 조각은 네트워크를 지나가는 길목과 서버 쪽에서 키를 지켜 주지만 내 기기와 주소를 다루는 사람의 습관까지 지켜 주지는 못합니다.
왜 물음표(?)가 아니라 샤프(#)일까
키를 주소에 담고 싶다면 ?key=... 처럼 물음표 뒤에 넣을 수도 있습니다. 하지만 그 경우 키는 요청 줄에 그대로 실려 서버로 전송됩니다. 서버 운영자가 의도하지 않아도 접속 기록, 오류 추적 도구, 앞단의 프록시, 보안 장비의 로그에 키가 남을 수 있습니다. 또 다른 사이트로 넘어갈 때 Referer 헤더에 물음표 뒤 내용이 실려 나갈 수도 있습니다.
샤프 뒤에 두면 이런 경로가 구조적으로 차단됩니다. 서버가 선의로 로그를 남기지 않겠다고 약속하는 것과, 받을 수조차 없는 것은 전혀 다른 수준의 안전입니다. 앞의 것은 신뢰에 기대는 방식이고 뒤의 것은 구조에 기대는 방식입니다. 이 서비스는 가능한 한 뒤의 방식을 택하려 했습니다.
다만 번역기 화면이 열린 뒤에는 브라우저 안의 자바스크립트가 이 조각을 읽어 키를 꺼내 씁니다. 조각을 읽을 수 있는 쪽은 서버가 아니라 그 페이지의 스크립트이므로, 페이지가 어떤 스크립트를 실행하는지가 중요해집니다. 그래서 앞서 설명한 CSP로 이 사이트가 직접 제공하는 스크립트만 실행되도록 막아 두었습니다.
실천할 수 있는 대비책
- 번역기 주소는 친구에게 처음 한 번만 전달하고, 이후 대화에서는 비밀어만 주고받으세요.
- 공용 컴퓨터에서는 시크릿 창을 쓰거나 사용 후 방문 기록을 지우세요.
- 주소가 오갈 대화방과 비밀어가 오갈 대화방이 같다면 한쪽이 털릴 때 모두 털립니다. 가능하면 주소는 다른 경로로 전달하세요.
- 중요한 내용이라면 번역기에 비밀번호를 거세요. 비밀번호를 건 번역기는 주소만으로는 열리지 않습니다.
- 자동 삭제 시간을 정해 두면 번역기 자체가 서버에서 사라집니다.
이 방식의 한계를 더 폭넓게 알고 싶으시다면 이 서비스가 막아 주는 것과 막아 주지 못하는 것을 함께 읽어 보시기 바랍니다. 직접 써 보고 싶으시면 번역기 열기로 이동하시면 됩니다.
잘못된 내용이나 보완할 점을 발견하셨다면 소개·문의의 이메일로 알려 주세요. 확인 후 글을 고치고 수정일을 갱신합니다.