먼저 결론입니다. 해시 뒤에 키를 두기 좋은 이유는, 브라우저가 HTTP를 만들 때 프래그먼트를 버리기 때문입니다. 새로운 암호 규약이 생긴 것이 아닙니다. 확인 방법은 주소창과 Network 요청 줄을 나란히 보는 것입니다. 전체 링크를 카카오톡, 메일, 티켓, 브라우저 히스토리에 붙이면, 그 문자열을 보는 사람에게는 이 층이 더 이상 성립하지 않습니다.
검색 의도를 한 문장으로 먼저 답하기
URL 해시 뒤에 키를 둘 수 있는 이유는 RFC 9110이 요청의 대상 URI에서 프래그먼트를 제외하기 때문입니다. 브라우저가 서버에 문서를 요청할 때 #와 그 뒤를 뗍니다. 호스팅, CDN, 접속 로그가 설계상 보는 것은 경로와 물음표 뒤 쿼리이며, 해시 뒤의 키가 아닙니다.
이 글은 개발자, 운영, 그리고 일회용 링크를 동료에게 보내기 전에 「서버는 못 본다」와 「채팅 기록은 못 본다」를 나누고 싶은 사람을 위한 것입니다. 열람 후 파기 사용 설명을 대신하지 않으며, 「링크를 어떻게 만드는가」도 반복하지 않습니다. 가져가야 할 것은 물음표와 해시가 각각 어느 층에 들어가는지, Network에서 어떻게 대조하는지, 전체 URL을 전달할 때 어느 보호가 사라지는지입니다.
쿼리는 HTTP에 들어가고, 해시는 기본적으로 탭에 남습니다
한 주소에 쿼리와 프래그먼트를 함께 둘 수 있습니다. 형태는 https://example.com/s.html?id=abc#key입니다. 물음표 뒤 id=abc가 쿼리(쿼리스트링)이고, 해시 뒤 key가 프래그먼트입니다. 브라우저가 문자열을 해석할 때 프래그먼트는 첫 번째 #부터 끝까지입니다. 그보다 앞 구간에 글자 그대로의 해시를 쓰려면 %23으로 써야 합니다. 그렇지 않으면 해석이 너무 이르게 잘립니다.
문서 요청을 보낼 때 브라우저가 조립하는 것은 Host, 경로, 쿼리입니다. MDN의 URI fragment 설명은 단정적입니다. 자원을 요청할 때 프래그먼트는 서버로 가지 않으며, 클라이언트가 문서를 받은 뒤에 처리합니다. HTML에서는 흔히 어떤 id로 스크롤하는 데 씁니다. 열람 후 파기 페이지에서는 같은 자리를 빌려, 이 탭이 로컬 복호화에 쓸 키를 실습니다.
그러므로 「서버는 암호문만 보관한다」가 성립하려면 조회용 번호는 쿼리로, 복호화 키는 프래그먼트로 가고, 페이지 스크립트가 location.hash를 이후 요청에 다시 쓰지 않아야 합니다. 하나라도 빠지면 해시는 장식일 뿐입니다.
규격이 말하는 것은 기본 동작이지, 추가 암호화가 아닙니다
RFC 3986 §3.5는 프래그먼트를 「주 자원 위의 이차 자원」을 가리키는 식별자로 두고, 클라이언트가 문서 유형에 따라 해석한다고 적습니다. RFC 9110 §7.1은 이어서, 대상 URI가 참조에 있는 프래그먼트를 제외한다고 말합니다. 프래그먼트 식별자는 클라이언트 처리용으로 남겨 두기 때문입니다. 두 문서를 겹치면 뜻은 하나입니다. # 뒤는 HTTP 요청 대상의 일부가 아닙니다.
접속 로그에 #가 없는 이유도 여기 있습니다. 운영이 필드를 숨긴 것이 아니라, 요청 줄에 그 구간이 원래 없습니다. 서버 로그로 「사용자가 어떤 앵커를 눌렀는지」를 세려 하면 같은 이유로 실패합니다. 같은 로그로 「키가 서버에 들어오지 않았다」를 보이려 하면, 규격과 맞습니다.
Referer도 같은 원칙을 따릅니다. MDN의 Referer 설명은, 이 헤더가 출처·경로·쿼리를 실을 수 있지만 URL 프래그먼트와 사용자 이름·비밀번호는 넣으면 안 된다고 합니다. Referrer Policy는 URL을 Referer로 줄일 때 먼저 프래그먼트를 비웁니다. 따라서 열람 페이지가 이후에 이미지나 스크립트를 가져가더라도, 기본적으로 해시 뒤의 키를 제3자 호스트에 넘기지 않습니다. 이는 브라우저의 기본 제거이지, 이 사이트가 만든 규약이 아닙니다.
페이지 스크립트는 여전히 프래그먼트를 읽습니다. MDN의 URL.hash는 #와 프래그먼트 자체이며, 현재 페이지는 location.hash로도 같습니다. 열람 페이지는 바로 이 인터페이스로 키를 꺼낸 뒤, Web Crypto로 AES-256-GCM 복호화를 합니다. 같은 인터페이스는 잘못 쓸 수도 있습니다. 스크립트가 해시를 쿼리에 붙이거나, 접속 분석 보고에 넣을 수 있습니다. 규격이 막는 것은 「브라우저가 기본적으로 만드는 요청 줄」이지, 「페이지가 읽어서 다시 보내는 것」이 아닙니다.
실제 비밀번호, API Key, 마스킹하지 않은 접속 문자열로 실험하지 마세요. 이번만 쓰는 테스트 문장을 준비하세요. 확인하는 것은 요청 줄과 요청 본문이지, 자격 증명을 다시 드러내는 일이 아닙니다.
Network에서 주소창과 요청 줄을 대조하기
슬로건은 증거가 되지 않습니다. 그 자리에서 끝낼 수 있는 확인은 개발자 도구의 Network를 열고, 주소창 전체 문자열과 각 요청 URL을 나란히 보는 것입니다. 이전 글은 카나리아로 원문을 찾았습니다. 이 글은 프래그먼트만 봅니다. # 뒤 구간은 주소창에 있어야 하고, 요청 줄에는 없어야 합니다.
- 열람 페이지를 열고 주소창이
s.html?id={id}#{key}형태인지 확인하세요. 쿼리에는 번호만, 키는 해시 뒤에만 있어야 합니다. - Network를 열고 Preserve log를 켠 뒤, 필터는 먼저 All로 두세요. 문서 요청, 이후 인터페이스, 접속 분석을 처음부터 숨기지 마세요.
- 해시 뒤의 키를 카나리아로 필터에 붙여넣으세요. 빨간 적중이 나오면 그 요청을 읽고 멈추세요. 구현이 프래그먼트를 쿼리로 잘못 썼거나, 스크립트가 읽은 뒤 다시 보낸 것입니다.
- 적중이 없으면 문서 요청과
/secrets같은 인터페이스를 하나씩 여세요. 요청 줄에는id만 있어야 하고, 본문에는 원문이 없어야 합니다.
만들기 쪽에서도 한 번 할 수 있습니다. 이 기기에서 AES-256-GCM으로 암호화한 뒤, 나가는 필드는 설계상 암호문, 만료, 열람 횟수뿐입니다. 난수 키는 이후에 조립하는 공유 링크에만 나타나야 하며, POST JSON에는 없어야 합니다. FastPwd의 열람 후 파기는 이 경계를 따릅니다. 만들기와 읽기 모두 열어 바로 사용하며 계정이 없습니다. 약속은 어디까지나 약속입니다. 이번 Network에서 본 것을 기준으로 하세요.
%23과 진짜 #를 나누세요. 요청 줄에 %23 뒤 문자가 보이면, 그 구간은 경로나 쿼리로 나간 것이므로 서버가 봅니다. 인코딩되지 않은 #만이 프래그먼트를 시작합니다. 키를 ?key=나 /%23key로 쓰면 「해시에 숨긴 것처럼」 보일 수 있습니다. 로그에게는 이미 평문 쿼리입니다.
페이지 스크립트와 접속 분석은 여전히 해시를 읽을 수 있습니다
기본 HTTP 요청에 프래그먼트가 없다고 해서, 「이 탭의 어떤 스크립트도 보지 못한다」는 뜻은 아닙니다. 현재 문서의 JavaScript는 location.hash를 가집니다. 열람 페이지는 이 기기에서 복호화하려면 반드시 읽어야 합니다. 접속 분석 스크립트가 location.href 전체를 보고하면, 키가 그 추적 요청의 쿼리나 본문에 나타납니다.
그러므로 카나리아 검색을 업무 인터페이스에서 멈추면 안 됩니다. 접속 분석 경로를 따로 필터하고 쿼리와 본문을 여세요. 페이지 제목과 경로는 나와도 됩니다. 해시 뒤의 키, 검사 중인 비밀번호, 세척 전 원문은 나오면 안 됩니다. FastPwd의 접속 분석은 사이트 자체 트래커를 쓰며, localhost에서는 기본적으로 보내지 않습니다. 그래도 같은 카나리아로 보고에 프래그먼트 키가 없는지 확인하세요.
확장 프로그램, 기업 DLP, 일부 「AI 브라우저」도 주소창 전체를 읽을 수 있습니다. 이들은 지금 보고 있는 HTTP 요청 줄을 타지 않으므로, Network 패널이 대신 증명해 주지 않습니다. 동료에게 말할 문장은 이렇게 줄이세요. 「요청 줄과 보고를 검색했고, 해시 뒤의 키는 없었다.」 이를 「이 컴퓨터의 어떤 소프트웨어도 이 해시를 본 적이 없다」로 키우지 마세요.
이 층의 보호가 깨지는 때
프래그먼트가 막는 것은 「이번 문서 요청이 키를 호스팅에 넘긴다」는 점입니다. 아래 장면에서는 전체 URL을 본 사람이 이미 키를 가진 것과 같습니다. 규격은 도울 수 없습니다.
첫째, 전체 링크를 카카오톡, 메일, 티켓에 붙이는 경우입니다. 메시지 본문의 문자열에 해시 뒤가 들어 있습니다. 상대 클라이언트, 방 히스토리, 티켓 검색, 이후 내보내기는 모두 전체 문자열을 봅니다. 서버 입장에서는 키가 HTTP에 안 들어갔을 수 있습니다. 「채팅 기록을 열 수 있는 사람」에게는 보호가 이미 없습니다. 전체 URL은 가진 사람이 바로 쓰는 자격 증명입니다.
둘째, 브라우저 히스토리와 동기화입니다. 주소창에 보인 # 프래그먼트는 보통 로컬 히스토리에 들어갑니다. 동기화를 켠 브라우저는 같은 기록을 다른 기기로 보낼 수 있습니다. 탭을 닫는 것은 히스토리를 지우는 일이 아닙니다.
셋째, 리다이렉트가 프래그먼트를 가져갈 수 있습니다. 대상 Location에 자체 프래그먼트가 없으면, 많은 브라우저가 원래 프래그먼트를 새 주소 뒤에 붙입니다. 키는 「이미 민감한 페이지를 떠났다」고 생각한 주소창에 남습니다. 열람 페이지가 분석 도메인이나 로그인 도메인으로 리다이렉트되면, 먼저 Network에서 최종 요청 줄을 보고, 주소창에 무엇이 남았는지 보세요.
넷째, 링크 미리보기와 보안 스캐너는 해시 앞 구간을 가져갑니다. 채팅 앱이 미리보기를 만들 때, 규격상 프래그먼트를 미리보기 대상 호스트에 보내지 않지만 s.html?id=…는 요청합니다. 구현이 문서의 첫 GET이나 암호문의 첫 GET에서 바로 파기하면, 미리보기 봇이 수신자보다 먼저 횟수를 쓸 수 있습니다. 수신자가 열면 「이미 파기됨」만 보입니다. 키는 미리보기 호스트에 넘어가지 않았습니다. 탄 것은 열람 횟수이지, 프래그먼트의 비밀성이 아닙니다.
FastPwd의 열람 페이지는 이 두 단계를 나눕니다. 페이지를 열 때는 암호문이 아직 있는지만 묻고, 암호문을 가져가지 않습니다. 수신자가 「열고 보기」를 누른 뒤에야 번호로 암호문을 가져오고, 해시 뒤의 키로 이 기기에서 복호화합니다. 만들기 페이지와 열람 페이지 모두 계정이 필요 없습니다. 미리보기 봇은 보통 열람 페이지만 가져가고 버튼을 대신 누르지 않습니다. 「미리보기 = 파기」 확률은 낮아지지만, 모든 스캐너가 열람 페이지에서 멈춘다는 보장은 아닙니다. 횟수는 여전히 서버 측 카운터이며, 프래그먼트의 일부가 아닙니다.
| 누가 보는가 | # 뒤의 키를 보는가 | 이 층이 아직 성립하는가 |
|---|---|---|
| 문서 요청의 요청 줄 | 기본적으로 보지 못함 | 성립. Network로 대조 |
| 호스팅 접속 로그 / CDN | 기본적으로 보지 못함 | 성립. 단, 키가 쿼리에 들어가지 않아야 함 |
| 제3자로 가는 Referer | 기본적으로 보지 못함 | 성립. 규격이 프래그먼트를 제거 |
| 페이지 스크립트 / 접속 분석 | location.hash를 읽을 수 있음 |
구현에 따름. 카나리아로 body를 검색 |
| 채팅, 메일, 티켓 본문 | 전체 문자열을 봄 | 전달한 상대에게는 성립하지 않음 |
| 브라우저 히스토리 / 동기화 | 보통 봄 | 이 브라우저를 열 수 있는 사람에게는 성립하지 않음 |
| 링크 미리보기 봇 | 규격상 프래그먼트를 받지 못함 | 열람 횟수를 쓸 수 있음 |
증명할 수 있는 것과 없는 것
이번 점검이 뒷받침하는 결론은 좁습니다. 뒷받침할 수 있는 것: 지금 쓰는 이 브라우저, 이번 열기에서, 문서 요청과 관찰한 인터페이스 요청 줄에 해시 뒤의 키가 없었고, 만들기 요청 본문에 원문이 없었습니다.
뒷받침할 수 없는 것: 단톡방 관리자가 전체 링크를 저장하지 않았다. 브라우저 확장이 주소창을 읽지 않았다. 상대가 연 뒤 화면을 찍지 않는다. 암호문을 가져간 뒤 다른 백업에 다시 나타나지 않는다. 열람 후 파기가 지우는 것은 서버의 암호문입니다. 이미 화면에 복호화된 평문도, 채팅 기록의 전체 URL도 지우지 않습니다.
프래그먼트를 「수신자만 열 수 있다」로 읽지 마세요. ?id=…#키 전체 문자열을 가진 사람은 공개 열람 페이지에서 복호화할 수 있습니다. 열람 페이지에 로그인 문이 없는 것은 설계입니다. 상대가 먼저 가입할 필요가 없습니다. 대가는 전체 문자열을 자격 증명처럼 보관해야 한다는 점입니다. 단체방에 그냥 넘기는 문서 링크처럼 다루면 안 됩니다.
자주 묻는 질문
URL 해시(#) 뒤 내용은 서버로 전송되나요?
기본적으로 아닙니다. RFC 9110은 대상 URI에 프래그먼트를 포함하지 않는다고 적습니다. 브라우저가 문서를 요청할 때 #와 그 뒤를 뗍니다. 서버, 리버스 프록시, 접속 로그가 설계상 보는 것은 경로와 물음표 뒤 쿼리뿐입니다. 요청 줄에 해시 뒤의 키가 보이면 구현 오류이지, 규격의 예외가 아닙니다.
복호화 키를 쿼리스트링에 두면 안 되는 이유는 무엇인가요?
물음표 뒤 쿼리는 HTTP 요청 줄에 쓰입니다. 호스팅, CDN, 접속 로그가 모두 봅니다. 키가 쿼리에 들어가는 순간, 「서버는 암호문만 보관한다」는 말은 로그 층에서 이미 성립하지 않습니다. 번호는 쿼리에 둘 수 있습니다. 암호문을 가져오는 식별자이기 때문입니다. 키는 안 됩니다.
해시가 붙은 전체 링크를 채팅에 붙여도 안전한가요?
서버는 그 HTTP 요청에서 여전히 키를 보지 못합니다. 채팅 기록, 티켓, 브라우저 히스토리는 봅니다. 전체 URL은 가진 사람이 바로 쓰는 자격 증명입니다. 문자열 전체를 가진 사람은 열람 페이지에서 복호화할 수 있습니다. 격리가 더 필요하면 「id가 있는 링크」와 「해시 뒤의 키」를 두 채널로 나누고, 같은 방에 전체 문자열을 던지지 마세요.
열람 후 파기 열람 페이지를 열려면 로그인이 필요한가요?
필요하지 않습니다. 열람 페이지는 수신자에게 공개됩니다. 물음표의 id로 암호문을 가져오고, 해시 뒤의 키로 이 기기에서 복호화합니다. FastPwd에는 계정과 비밀번호 보관함이 없으며, 만들기와 읽기 모두 열어 바로 사용합니다. 로그인 문은 프래그먼트 경계를 해결하지 않으며, 이 층의 점검에 넣어서도 안 됩니다.
해가 없는 테스트 링크로 그 자리에서 확인하기
쿼리와 프래그먼트를 이미 나눈 페이지로 연습하고 싶다면 FastPwd의 열람 후 파기부터 시작하세요. 열어 바로 사용하며 가입이 필요 없습니다. 버려도 되는 테스트 문장을 쓰고, 이 기기에서 암호화한 링크를 만드세요. 만들기 요청에는 암호문과 횟수·만료 같은 필드만 보여야 합니다. 공유 링크가 s.html?id={id}#{key} 형태입니다.
그 문자열로 열람 페이지를 여세요. 주소창에는 물음표와 해시가 함께 있어야 합니다. Network에서 문서 요청과 상태 조회는 id만 실어야 합니다. 「열고 보기」를 누른 뒤에도 암호문을 가져가는 요청에 해시 뒤의 키가 있으면 안 됩니다. 평문은 이 기기에서 복호화한 뒤에만 페이지에 나타나야 합니다. 알고리즘은 AES-256-GCM이며, 계산은 Web Crypto에서 합니다. 텍스트 한 건은 32 KB까지입니다. 파일 전체는 파일 암호화를 쓰세요. 단일 파일 5 GB 이하, 출력은 .lock / .enc입니다.
이 연습의 목적은 어떤 사이트가 「절대 안전하다」고 증명하는 것이 아닙니다. 두 층을 나누는 것입니다. 첫째 층: 호스팅은 규격상 프래그먼트를 보지 못합니다. 둘째 층: 전체 링크를 본 사람은 키를 가진 것과 같습니다. 「키가 해시 뒤에 있다」고 적힌 어떤 페이지에서도 절차는 같습니다. 주소창과 요청 줄을 대조하고, 접속 분석은 따로 본 뒤, 이 URL이 채팅 기록에 들어갈지를 스스로 물으세요.
다음에 보내기 전에 세 줄을 기억하세요. 첫째, 번호는 쿼리에 넣어도 되고, 키는 해시에만 넣습니다. 둘째, 요청 줄이 깨끗하다는 것은 서버가 키를 받지 않았다는 뜻일 뿐입니다. 셋째, 전체 링크는 자격 증명처럼 보관하고, 평범한 문서 링크로 전달하지 마세요. 반복할 수 있는 관찰만 여러분 자신의 인수인계에 적을 가치가 있습니다.
더 읽을거리
이전 글은 「원문이 이 탭을 떠났는가」를 반복할 수 있는 Network 점검으로 줄입니다. 이 글과 나란히 읽기 좋습니다. 브라우저 로컬 암호화에서 원문이 이 기기를 떠나지 않았는지 확인하는 방법. 이 글은 물음표와 해시를 나누고, 전체 링크를 전달할 때 깨지는 층만 적습니다.