Safari가 URL이 없는 빈 탭을 열자, 보안 검사에서 '본인이 아님'으로 인식했습니다.
요약
AI 에이전트가 Mac의 Safari를 구동하는 MCP 서버에서 보안 검사 오류가 발생했습니다. 빈 URL의 탭을 열고 다음 명령을 실행할 때, 시스템은 '본인이 아님'으로 인식하며 명령을 거부했습니다. 이는 Safari가 빈 탭의 URL 정보를 누락하여 발생하는 버그였습니다.
핵심 포인트
- Safari는 빈 탭에 대해 URL 정보(origin)를 누락합니다.
- 이로 인해 에이전트의 다음 명령 실행이 '본인이 아님'으로 간주되어 거부됩니다.
- 개발자는 이 문제를 해결하기 위해 Safari의 동작 방식을 정확히 이해해야 합니다.
저는 AI 에이전트가 Mac의 실제 Safari를 구동할 수 있는 MCP 서버인 safari-mcp를 유지 관리하고 있습니다. 이 서버는 로그인 세션까지 포함합니다. 10월 7일, 한 에이전트가 빈 탭을 열었고, 그 탭에서 실행된 바로 다음 명령이 거부되었습니다:
receipt is not valid for this origin
에이전트는 모든 것을 올바르게 수행했습니다. URL 없이 safari_new_tab()을 호출했고, 도구가 반환한 리시버(receipt)를 유지했으며, 이를 safari_navigate로 전달했습니다. 탭은 탐색될 수 없었고, 리시버는 회전될 수 없었으며, 탭도 닫힐 수 없었습니다. 이 탭은 이름이 지정된 Safari 프로필에 그대로 남아 있었고, 에이전트가 할 수 있는 어떤 작업도 그것을 건드릴 수 없었습니다.
저는 그날 두 개의 버전을 출시했고, 각각 코드를 한 줄씩 변경했습니다. 두 줄 모두 의도적으로 엄격하게 만든 가드(guard) 안에 있었습니다.
리시버란 무엇인가
safari-mcp Safari 확장 프로그램이 에이전트를 위해 탭을 열 때, 해당 탭에 대한 불투명한 리시버를 발행합니다. 이후 모든 명령은 이 리시버를 가지고 실행되며, 무언가를 실행하기 전에 확장 프로그램은 두 가지를 확인합니다:
- 리시버가 이 탭에 속하는지,
- 이 탭이 리시버가 발행된 원본 출처(origin)에 여전히 있는지.
두 번째 확인 사항이 중요합니다. 에이전트는 사용자 이메일, 은행 및 관리자 패널을 보유한 브라우저 내에서 작동합니다. 만약 한 페이지가 에이전트의 탭을 다른 곳으로 리디렉션한다면, 저는 에이전트의 다음 evaluate 명령이 로드된 내용에 대해 실행되는 것이 아니라 거부되기를 원합니다. 계속 작동하려면, 에이전트는 의도적으로 새로운 리시버를 요청(getReceipt)해야 하며, 확장 프로그램은 탭에 http(s) 출처 또는 about:blank을 읽을 수 있을 때만 하나를 발행합니다.
실패 시 폐쇄(Fail closed). 서류상으로는 그것이 정확히 원하는 동작입니다.
버그는 누락된 하나의 값 때문이었습니다
수정 전, 확장 프로그램은 다음과 같은 방식으로 탭의 출처(origin)를 파생했습니다:
function _receiptOrigin(rawUrl) {
const raw = String(rawUrl || "");
if (raw === "about:blank") return raw;
...
빈 탭의 리시프트(receipt)는 about:blank에서 생성됩니다. 저는 Safari가 빈 탭의 URL을 about:blank로 보고할 것이라고 가정했습니다. 하지만 그렇지 않습니다. Safari는 아예 탭에서 url 정보를 누락합니다.
String(undefined || "")은 ""입니다. new URL("")은 오류를 발생시킵니다. 이 함수는 ""를 반환하며, 이는 코드상 "어떠한 오리진도 없음"을 의미합니다. "about:blank" !== ""이므로, 검사 로직은 탭 자체의 리시프트를 거부했습니다. 탈출구(escape hatch)라고 불리는 기능 역시 동일한 함수를 호출하여 같은 ""를 받았고, 이 또한 다음과 같이 거부되었습니다: Cannot issue a receipt for this tab URL.
가드(guard)는 제가 구축하도록 만든 대로 작동했습니다. 단지 "이 탭이 어딘가로 이동했다"와 "Safari가 이 탭의 위치를 알려주지 않았다"라는 두 상황을 구분할 수 없었고, 둘 다 공격으로 간주했습니다.
형제 버그(The sibling bug), 하루 전 사건
10월 6일, 다른 실행 과정에서 이름이 지정된 프로필에 두 개의 탭이 열렸는데, 모두 claude.ai/new였습니다. 두 경우 모두 페이지 자체가 리디렉션이나 로그인 바운스 등을 통해 탭을 이동시킨 상황이었습니다. 로테이션(Rotation)은 새로운 페이지가 확장 프로그램이 읽을 수 있는 http(s) 오리진을 가지고 있을 때만 작동합니다. 오리진이 없는 페이지에 도달하거나, Safari가 확장 프로그램에게 URL을 보여주지 않는 페이지의 경우 탭을 로테이션할 수 없습니다.
close_tab은 다른 모든 명령어와 마찬가지로 리시프트의 오리진(origin)에 종속되었습니다. 이 거부 메시지는 리시프트를 로테이션해야 함을 시사했는데, 이는 바로 이 두 개의 탭이 할 수 없는 것이었습니다. 아무것도 그들을 닫을 수 없었습니다.
빈 탭과 같은 형태입니다: 페이지 내에서 코드를 실행하는 명령어에는 적절한 규칙이 적용되었지만, 그렇지 않은 명령어(close_tab)에도 동일하게 적용되었습니다.
한 줄로 해결된 두 가지 수정 사항
v2.22.12, 이동된 탭에 대한 수정:
const allowReceiptOriginChange = type === "get_tab_receipt" || type === "close_tab";
탭을 닫는 것은 페이지 내에서 아무것도 실행하지 않기 때문에, 이제 close 명령어는 오리진(origin)이 아닌 리시프트(receipt)를 통해 자신의 탭을 찾습니다. 다른 모든 명령어는 여전히 리시프트의 오리진에 바인딩되어 있으며, 확장 프로그램이 발행한 적 없는 리시프트는 아무것도 닫지 못합니다.
v2.22.13, 빈 탭에 대한 수정:
if (!raw || raw === "about:blank") return "about:blank";
Safari가 URL이 없는 빈 탭을 열자, 보안 검사에서 '본인이 아님'으로 인식했습니다.
A Safari가 URL을 표시하지 않는 탭은 공백(blank)으로 간주됩니다. 웹 오리진(web origin)에서 발행된 영수증(receipt)도 그곳에서는 페이지 명령을 수행하지 않으므로, 실제 사이트에서 빈 페이지로 이동한 탭은 여전히 거부됩니다.
제가 잘못 이해했던 부분
오리진 바인딩 자체는 문제가 없었습니다. 문제는 그 안에 있던 두 가지 가정(assumption)이었습니다.
저는 '알 수 없음(unknown)'을 '불일치(mismatch)'로 취급했습니다. 실패 시 폐쇄(fail-closed) 검사는 최소한 세 가지 결과가 있습니다: 일치함, 불일치함, 또는 플랫폼이 결정할 만큼 충분한 정보를 제공하지 않음. 제 코드는 이 세 번째 경우를 두 번째 경우에 합쳤습니다. 왜냐하면 URL을 읽는 함수가 'URL 미보고(no URL reported)'와 '신뢰할 수 없는 오리진(an origin I won't trust)' 모두에 대해 동일한 빈 문자열을 반환했기 때문입니다. 이 두 가지가 값이 공유되면, 가드(guard)는 언젠가 소유자를 차단하게 됩니다. 해결책은 코드로 결정하는 것이었습니다. 즉, 확장 프로그램이 생성한 탭의 누락된 URL이 무엇을 의미하는지: 그것은 공백이라는 것입니다.
저는 모든 명령에 동일한 규칙을 적용했습니다. 오리진 검사는 코드가 잘못된 페이지에서 실행되는 것을 막기 위해 존재합니다. close_tab은 어떤 페이지에서도 코드를 실행하지 않습니다. 여기에 오리진 바인딩을 추가하는 것은 보호 기능을 추가하지 않았고, 유일한 탈출구마저 없애버렸습니다. 가드는 자신이 방지하려는 피해만큼 좁아야 하며, 모든 가드는 자신이 지키는 것과 무관하게 작동하는 출구가 필요합니다.
`url:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기