미리보기가 뜨는 채팅에 일회용 링크를 붙인 뒤, 읽기 횟수가 이미 쓰였다고 볼 수 없는 이유
채널에 카드가 먼저 떴다고 해서, 상대가 암호문을 읽은 것은 아닙니다. 슬랙, 디스코드, 카카오톡, Teams는 미리보기를 그리려고 페이지에 HTTP GET을 보냅니다. 이 요청이 읽는 것은 제목과 짧은 설명인 경우가 대부분이며, 「열어 보기」는 누르지 않고 주소창 # 뒤의 키도 가져가지 않습니다. 아래에서는 「카드가 나왔다」와 「읽기를 한 번 썼다」를 가른 뒤, 그 자리에서 대조할 수 있는 단계를 적습니다. 일회용 링크 만들기 페이지의 조작 안내가 아닙니다.
먼저 카드가 가져가는 층을 가릅니다
이 글이 답하는 것은 「미리보기가 뜨는 채팅에 붙인 뒤, 읽기 횟수가 이미 쓰였다고 볼 수 없는 이유」뿐입니다. 일회용 링크의 폼 설명도 아니고, 「#가 서버 로그에 안 남는 이유」의 층도 아닙니다.
링크 미리보기가 가져가는 층
카드는 보내기를 누른 전후 몇 초 사이에 뜹니다. 움직이는 것은 채팅 제품의 서버이지, 상대의 손가락이 아닙니다.
https://…/s.html?id=…#… 를 슬랙, 디스코드, 카카오톡, Microsoft Teams에 붙이면, 입력창이나 메시지 아래에 작은 카드가 뜨는 경우가 많습니다. 제목, 한 줄 설명, 가끔 아이콘입니다. 이 카드를 「링크가 이미 열렸다」로 읽는 사람이 많지만, 순서는 반대입니다. 채팅 제품은 먼저 그 주소로 HTTP GET을 보내고, 돌아온 HTML에서 og:title, og:description, 또는 평범한 <title>을 뽑은 뒤에야 카드를 그립니다. 상대는 그 시점에 아직 들어가지 않은 경우가 대부분입니다.
슬랙은 이 동작을 공식으로 적어 두었습니다. 기본값에서는 이용자나 Slack 앱이 보낸, 완전한 URL이 들어 있는 메시지가 링크를 펼칩니다. 미디어도 펼칠 수 있습니다. 설명은 Slack 문서 《Unfurling links in messages》에 있습니다. 링크를 펼치는 봇은 자신을 Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)이라고 밝힙니다. 같은 쪽 Slack Robots는 페이지를 되도록 적게 가져가고(HTTP Range를 씀), 목적은 oEmbed, Twitter Card, Open Graph 태그라고 적습니다. 태그가 이미지·영상·오디오를 가리키면 그 파일을 확인하려고 한 번 더 가져갑니다. 응답은 대략 30분 캐시됩니다. 문서는 「페이지의 JavaScript를 실행한다」거나 「이용자 대신 버튼을 누른다」고 쓰지 않습니다.
카카오톡은 같은 일을 「스크랩」이라고 부릅니다. 카카오 데브톡 《스크랩(미리보기)》에 따르면, 채팅방에 URL을 입력하거나 공유하면 스크랩 서버가 원본 페이지에 직접 접근해 OG 태그, title, description, 대표 이미지를 모읍니다. 미리보기에 쓸 최소 정보만 빠르게 가져간다고 적혀 있습니다. 스크랩봇 User-Agent는 facebookexternalhit/1.1;kakaotalk-scrap/1.0입니다. 슬랙과 같이, 문서는 스크립트 실행이나 버튼 클릭을 말하지 않습니다.
브라우저가 이 GET을 보낼 때 # 이후는 요청 줄에 들어가지 않습니다. IETF RFC 3986 §3.5는 fragment를 클라이언트 쪽 식별자로 두고, 서버가 URI를 처리할 재료로 쓰지 않습니다. 미리보기 봇이 채팅창에 보이는 문자열을 그대로 복사하더라도, UsePwd에 도착하는 요청은 대개 s.html?id={번호}까지입니다. 키가 접속 로그에 안 남는 이유는 기존 글 URL의 # 프래그먼트가 서버 로그에 나타나지 않는 이유에 있습니다. 이 글이 보태는 것은 다음 층입니다. 키를 가져가지 않은 이 GET이 「한 번 읽었다」가 되는가.
미리보기에서 물을 것은 「채팅 제품이 이 URL을 봤는가」가 아닙니다. 「이 GET이 암호문 조회 API까지 갔는가」입니다. 카드가 나온 것은 제목이 읽혔다는 증거일 뿐입니다. 암호문이 아직 있는지는 다른 대조입니다.
첫 GET이 읽기가 아닌 이유
「누가 암호문을 가져가는가」와 「누가 읽기 페이지를 열었는가」를 같은 요청에 묶으면, 미리보기 봇이 유일한 읽기를 써 버립니다. 층을 나눈 뒤 이 둘은 같은 동작이 아닙니다.
한 번만 쓰는 링크 가운데는, 미리보기 대상인 그 URL에 본문을 매달아 두는 구현이 있습니다. 봇이 한 번 GET하면 서버는 본문을 돌려주고 기록을 지웁니다. 상대가 나중에 열면 「이미 읽힘」이거나 404입니다. 이 실패는 흔합니다. 슬랙이 「악의로 비밀을 태운」 것이 아니라, RFC에서 안전하고 캐시할 수 있어야 하는 GET을 「암호문을 파기하는 동작」으로 쓴 구현 때문입니다.
UsePwd의 일회용 링크는 세 단계를 나눕니다. 만들 때 현재 탭이 Web Crypto로 AES-256-GCM 암호화를 합니다. 평문 상한은 32 KB입니다. 올리는 필드는 암호문, 만료 시간, 읽기 횟수뿐입니다. 만료는 1시간, 24시간, 7일, 또는 「읽은 뒤에만 파기」입니다. 읽기 횟수는 1–10, 기본값은 1입니다. 링크 형태는 s.html?id={id}#{key}입니다. 만들기 페이지와 읽기 페이지 모두 가입 없이 바로 사용합니다. 서버가 잠시 보관하는 것은 암호문뿐이며, 평문은 보이지 않고, 이용자별로 되찾을 계정도 없습니다.
상대가 읽기 페이지를 열면, 페이지는 먼저 /api/secrets/{id}/status에 암호문이 아직 있는지를 묻습니다. 돌아오는 값은 active, burned, expired뿐이며, 읽기 횟수는 늘어나지 않고 기록도 지우지 않습니다. 「열어 보기」를 눌렀을 때만 브라우저가 /api/secrets/{id}를 호출해 암호문을 가져오고, # 뒤의 키로 이 기기에서 복호화합니다. 만들 때 정한 횟수에 도달하면 서버가 암호문을 삭제합니다. 다시 가져오면 410입니다. 미리보기 봇은 읽기 페이지 HTML에서 멈추며, 흔한 경우에는 키도 없고 그 버튼도 누르지 않습니다.
정적 읽기 페이지를 반환합니다. 제목은 「일회용 링크 열기」이며 평문은 없습니다. 미리보기 카드는 대개 이 층에서 멈춥니다.
아직 있음, 파기됨, 만료됨만 답합니다. 여러 번 물어도 횟수는 늘지 않습니다.
여기서 비로소 읽기 1회가 됩니다. 기본값은 1회 뒤 삭제입니다. 읽기 페이지에서는 「열어 보기」에 대응합니다.
읽기 페이지에는 그 자리에서 대조할 수 있는 문장도 있습니다. 버튼을 누르면 「읽기 횟수를 한 번 쓸 수」 있습니다. 스크립트가 없으면 복호화할 수 없고, 페이지는 <noscript>로 이를 설명합니다. 미리보기 봇이 HTML만 읽고 스크립트를 실행하지 않으면 상태 조회조차 보내지 않습니다. 앱 안 브라우저가 스크립트를 실행하더라도, 먼저 가는 곳은 「암호문이 남아 있습니다」까지입니다. 암호문을 가져오려면 여전히 버튼이 필요합니다.
어떤 봇이 오고, 무엇을 가져가나
카드를 그릴 수 있는 제품은 많습니다. 가져가는 것은 페이지 머리글이지, # 뒤의 키도, 복호화된 평문도 아닙니다.
슬랙의 링크 펼치기 봇, 디스코드의 임베드용 크롤러, 카카오톡 스크랩 서버, Teams가 URL을 한 장의 카드로 묶는 서버 측 요청은 같은 종류입니다. 먼저 페이지를 GET하고, HTML에서 제목과 설명을 찾습니다. 디스코드는 그 크롤러를 Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)으로 밝힙니다. 공개된 Discordbot 설명은 서버가 돌려준 HTML을 파싱하며 JavaScript는 실행하지 않는다는 한계를 공통으로 둡니다. 슬랙은 다른 한계도 분명히 합니다. robots.txt를 검색 크롤러처럼 지키지 않습니다. 링크 펼치기는 「링크를 붙인 사람을 대신해 움직이는」 일이며, 사이트를 따라 걷는 수집이 아니라는 이유입니다. 읽기 페이지에 noindex를 붙여도 검색엔진 등록은 막을 수 있어도, 채널의 카드는 막지 못합니다.
UsePwd의 읽기 페이지는 임시 암호문 장면이며 noindex, nofollow를 붙이고 사이트맵에도 넣지 않습니다. 페이지 제목은 「일회용 링크 열기」로 고정하고, 설명은 복호화가 현재 탭에서 끝난다는 것과 키가 # 뒤에 있다는 것만 적습니다. 미리보기 카드가 만들어지면 채널에 나오는 것은 이 공통 문장입니다. 적어 넣은 비밀번호, 토큰, 티켓 원문은 나오지 않습니다. 이는 「서버 로그에 키가 있는가」와 동시에 성립합니다. 카드는 누군가 읽기 페이지를 GET했다는 증거입니다. 로그에 fragment는 그래도 없습니다.
슬랙은 같은 URL의 펼치기 결과를 대략 30분 캐시한다고 적습니다. 짧은 시간에 같은 테스트 링크를 채널에 여러 번 붙여도 두 번째 가져오기는 안 보일 수 있습니다. 「봇이 왔는가」를 대조하려면 카드를 몇 번 새로고침했는가가 아니라, 자신의 접속 로그에 Slackbot-LinkExpanding이나 kakaotalk-scrap 같은 User-Agent가 있는지를 보세요. s.html에 대한 GET이 로그에 있어도, 「/api/secrets/{id}가 호출되었다」고 말할 수는 없습니다.
정말로 횟수가 줄어들 때
「반드시 줄지 않는다」는 「영원히 줄지 않는다」가 아닙니다. 「열어 보기」까지 닿는 방문만 암호문 조회의 층에 들어갑니다.
가장 흔한 소비자는 상대 본인입니다. 전체 링크를 열고 「암호문이 남아 있습니다」를 본 뒤 「열어 보기」를 누릅니다. 기본 횟수는 1입니다. 이 한 번이 성공한 뒤 서버는 암호문을 삭제합니다. 같은 링크를 다시 열면 「파기됨」이 나옵니다. 만들 때 횟수를 2 이상으로 두면, 상한에 닿을 때까지 암호문은 남습니다. 기한이 되어도 삭제됩니다. 기본은 24시간이며, 1시간 또는 7일로 둘 수도 있습니다. 「읽은 뒤에만 파기」를 고르면 시계가 아니라 횟수로만 지웁니다.
두 번째 종류는 스크립트를 실행하고 클릭까지 흉내 내는 환경입니다. 일부 메일 보안 게이트웨이나 링크 샌드박스는 격리 브라우저에서 URL을 열고, 페이지의 주 버튼까지 누르기도 합니다. 그것은 「카드를 한 장 그리는」 일이 아니라, 사람과 같은 또 한 번의 완전한 방문입니다. 샌드박스가 「열어 보기」를 누르면 횟수는 줄고, 상대는 그 뒤에 파기됨을 봅니다. 이 층은 게이트웨이 자신의 보고로 대조하세요. 채팅 카드에서 거꾸로 계산할 수는 없습니다.
세 번째 종류는 암호문 조회 API 주소 자체를 내보내는 일입니다. 읽기 페이지 주소는 s.html?id=입니다. 횟수를 쓰는 것은 /api/secrets/{id}입니다. 평범한 이용자는 후자를 복사하지 않습니다. 누군가 API URL을 미리보기가 뜨는 채널에 붙이고, 그 제품이 첫 GET을 암호문 조회와 같게 다루면, 미리보기 단계에서 횟수가 줄어듭니다. UsePwd의 공유 형태는 그 주소가 아닙니다. 만들기 페이지가 복사하는 것은 #가 붙은 읽기 페이지 링크입니다.
미리보기가 횟수를 쓰지 않았다는 것은, 비밀이 상대에게만 있다는 뜻이 아닙니다. 채팅 제품은 전체 URL을 저장하고, 워크스페이스 관리자는 기록을 내보낼 수 있습니다. 횟수가 제한하는 것은 서버 위 암호문의 수명입니다. 채널에 남은 주소를 누가 다시 열 수 있는가가 아닙니다.
그 자리에서 미리보기를 대조하기
목표는 「세상의 모든 채팅 제품이 암호문을 읽지 않는다」를 증명하는 것이 아닙니다. 카드가 나온 뒤, 암호문 조회 API가 아직 호출되지 않았음을 보이는 것입니다.
-
01
실제 전달에 쓰지 않을 테스트 문구를 준비합니다
사용 중인 API 키나 로그인 비밀번호는 쓰지 마세요. 예를 들어 일회용 링크를 열고
PreviewCard-20260905를 넣은 뒤, 횟수는 기본값 1, 만료는 24시간으로 둡니다. 페이지는 가입 없이 바로 사용합니다. 아래 단계는 미리보기와 API를 대조하기 위한 것이며, 이 문자열을 동료에게 정식 키로 넘기지 않습니다. -
02
전체 링크를 적어 두고, 먼저 직접 읽기 페이지를 엽니다
만든 뒤에는
s.html?id=와#두 구간이 보여야 합니다. 같은 링크를 지금 브라우저에서 엽니다. 페이지는 「암호문이 남아 있습니다」에서 멈추고 「열어 보기」가 나옵니다. 먼저 버튼은 누르지 마세요. F12로 네트워크 패널을 열면/api/secrets/{id}/status요청이 보여야 합니다. 이 시점에는/status가 붙지 않은/api/secrets/{id}암호문 조회가 나와서는 안 됩니다. -
03
같은 링크를 미리보기가 뜨는 테스트 채널에 붙입니다
나만 보는 슬랙 채널, 디스코드 DM, 카카오톡 「나와의 채팅」을 씁니다. 붙인 뒤 카드가 뜰 때까지 기다립니다. 카드 제목은 「일회용 링크 열기」에 가깝고,
PreviewCard-20260905는 나와서는 안 됩니다. 읽기 페이지를 새로고침하거나, 버튼을 누르지 않은 다른 창에서 같은 링크를 다시 열면 여전히 「암호문이 남아 있습니다」에서 멈춰야 합니다. 여기서 「파기됨」이라면, 카드 자체가 읽기인 것이 아니라 다른 방문이 암호문 조회까지 간 것입니다. -
04
필요할 때만 접속 로그의 User-Agent를 봅니다
사이트 접속 로그를 볼 수 있다면, 미리보기 단계에서 흔한 것은
/ko/s.html?id=…에 대한 GET이며, User-Agent에Slackbot-LinkExpanding또는kakaotalk-scrap이 들어 있을 수 있습니다. 같은 시각에/api/secrets/{id}의 성공한 조회는 나와서는 안 됩니다. 슬랙은 같은 URL을 대략 30분 캐시하므로, 짧은 시간의 재붙여넣기는 두 번째 가져오기를 만들지 않을 수 있습니다. -
05
마지막에 「열어 보기」를 눌러, 횟수가 줄었음을 확인합니다
이제 버튼을 누릅니다. 네트워크 패널에 암호문 조회가 나오고, 페이지는 테스트 원문을 보여 줍니다. 닫은 뒤 같은 링크를 다시 열면 「파기됨」이 나옵니다. 이 단계는 「버튼 뒤」와 「카드 뒤」가 같은 상태가 아님을 대조하기 위한 것입니다. 확인이 끝나면 링크는 버리세요. 실제 상대에게 돌리지 마세요.
새 키를 다른 사람에게 넘기기만 하면 된다면, 평문을 채널에 붙여 「카드를 한 장 시험」하지 마세요. 테스트 문구와 실제 문구는 나눕니다. 실제 전달은 그래도 일회용 링크입니다. 환경변수가 유출된 뒤의 두 번째 바퀴는 클라우드 환경변수가 유출된 뒤, 새 키를 채팅창에 다시 보내면 안 되는 이유에 있습니다. 그 글은 「API 키 자체를 검색되는 기록에 붙이지 마라」입니다. 이 글은 「링크를 붙인 뒤, 미리보기 가져오기가 어느 층에서 멈추는가」입니다.
횟수가 남아도 채팅에 남는 것
「카드가 암호문을 태우지 않았다」를 「이 링크는 채널에서 사라졌다」로 읽으면, 같은 방식으로 대조할 수 있는 경계를 몇 가지 놓칩니다.
채팅 제품이 저장하는 것은 붙여 낸 주소 전체입니다. 기록을 검색할 수 있는 사람, 워크스페이스를 내보낼 수 있는 사람, 백업을 열 수 있는 사람은 읽기 페이지를 다시 엽니다. 횟수가 남아 있는 동안, 나중에 연 사람도 「열어 보기」를 눌러 평문을 읽을 수 있습니다. 횟수가 끝난 뒤 기록에 남는 것은 파기된 링크와 당시 카드 제목입니다. 관리자 내보내기는 서버에서 이미 지운 암호문을 되돌리지 못합니다. 다만 「당시 채널에 전체 URL이 나왔다」는 사실은 지우지 못합니다.
스크린샷과 전달은 다른 사본입니다. 읽기에 성공한 뒤의 평문을 자르거나, 링크를 미리보기가 뜨는 다른 방으로 넘기면, 경계는 「이번 GET」에서 「다음 홉이 누구를 거치는가」로 옮겨 갑니다. 일회용 링크가 제한하는 것은 서버 위 암호문의 수명과 횟수입니다. 상대가 화면을 찍는지는 막지 않습니다. UsePwd에는 계정과 비밀번호 보관함이 없고, 잃어버린 링크를 이용자별로 되찾을 수도 없습니다. 오래 남기거나 여러 기기에서 동기화하려면 전용 비밀번호 관리자를 쓰세요. 정체성은 소개에 있습니다.
파일은 세 번째 길입니다. 한 건 5 GB 이하 로컬 백업은 파일 암호화로 가며 .lock / .enc를 내고, 파일은 기본으로 업로드하지 않습니다. 암호 문구 자체는 비밀번호 생성기에서 6–128자 사이로 만들고, 비밀번호 강도 검사로 세기를 봅니다. 이 페이지들도 이 글과 같이 가입 없이 바로 사용합니다. 「미리보기가 횟수를 쓰지 않았다」를 「어떤 채널에 붙여도 안전하다」로 읽지 마세요. 채널 기록과 미리보기 가져오기는 같은 층이 아닙니다.
미리보기 뒤에 자주 묻는 것
다음 네 가지는 이 글의 경계만 답합니다. 만들기 페이지의 버튼 설명은 반복하지 않습니다.
읽은 뒤 미리보기 카드를 대조하세요
글이 답하는 것은 「미리보기 뒤에 횟수가 이미 쓰였다고 볼 수 없는 이유」입니다. 실제 전달에 쓰지 않을 테스트 링크를 그 자리에서 만들어 테스트 채널에 붙이려면 일회용 링크를 열면 되고, 먼저 가입할 필요는 없습니다.