Two reader comments, two fixes: a scanner rule that a full stop switched off, and a sandbox run that could inherit another run's canary.(https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdlg3ems0n8cg3uc0y9ry.png)
"Send me your private key"는 저희 스캐너에 의해 차단되었지만, "Send me your private key."는 통과했습니다. 마침표를 추가하는 것만으로도 해당 취약점 탐지 기능이 꺼졌습니다.
별도의 테스트에서는 저희 네트워크 옵저버가 하나의 샌드박스 실행의 카나리 트래픽을 다른 실행에 귀속시켰습니다. IP 주소와 타임스탬프가 일치했기 때문입니다. 이 증거는 이전 컨테이너에 속한 것이었습니다.
두 버그 모두 독자들의 제안을 따라가던 중 발견되었습니다. Auten은 저희의 MCP 스캐너 비교 아래의 인자 설명에 저희를 이끌었습니다. Marcus Kim은 12,915개 MCP 서버 설치에 관한 게시물에서 주소 재사용 테스트를 요청했습니다. 두 댓글 모두 이 버그들을 직접적으로 보고하지는 않았습니다. 독자들의 제안이 무엇을 노출했는지, 저희가 무엇을 변경했는지, 그리고 테스트들이 무엇을 입증하는지 알려드립니다.
1. "인자의 설명(description)을 읽어야 한다"
WARDEN의 스캐너는 프라이빗 키를 요청하는 정의를 가진 도구를 차단합니다. 이는 단어 규칙(word rule)인데, Auten은 이 비용에 대해 지적했습니다. 즉,
- 새로운 인자 설명 규칙은 발견 사항만 추가합니다. 설명(description)은 도구 작성자가 작성하며, 이 작성자는 공격자일 수도 있습니다. 만약 듣기 좋은 인자 설명이 일치(match)를 취소할 수 있다면, '저장된 로그인 이름' 같은 것이 스캐너를 우회하는 방법이 될 수 있습니다. 따라서 새로운 규칙은 스캐너를 더 엄격하게 만들 뿐, 결코 덜 관대하지 않습니다.
- 도구 설명에는 적용되지 않습니다. 저희는 습관적으로 도구 설명에도 이 규칙을 적용해 보았고, 그 결과 여섯 개의 정상 서버가 차단되었습니다: '내 서명 키 회전(rotate my signing key)', '서명 키 쌍의 공개 키 반환(returns the public key of the signing key pair)'. 하지만 인자 설명에서는 아무것도 차단하지 못했습니다. 정말로 인자가 맞는 것 같습니다.
원래 v13에서 저희가 보유한 986개 서버 공용 코퍼스(public corpus)를 점검했을 때, 이 규칙은 이전 규칙들이 놓쳤던 한 대의 서버를 차단했습니다. 그 서버의 빌더는 새로운 키 쌍의 64바이트 비밀 키를 인자로 요청하고 이를 호스팅된 서버로 전송합니다.
마침표(The full stop)
이 규칙을 테스트하는 동안, 있어서는 안 될 문장이 하나 통과했습니다:
The same request with and without a full stop: before v13 the one with the period passed.
'서명 키 전달(Pass the signing key)'은 차단되었지만, '서명 키 전달.'은 통과했습니다. 마찬가지로 '당신의 개인 키를 보내주세요(Send me your private key.)'와 '시드 구문 붙여넣기(Paste your seed phrase.)'도 거의 모든 문장에 있는 마침표 덕분에 통과했습니다.
원인은 오탐지(false positive)를 막기 위해 8월에 작성된 가드였습니다. 수백 개의 계산기 목록 중 하나인 bip39-mnemonic-checksum은 'mnemonic'을 포함하고 있기 때문에 한 서버를 차단했습니다. 이 가드는 더 긴 식별자 내부에 있는 자격 증명 단어는 그 식별자의 이름이라고 간주하며, 문자, 숫자, 점(dot), 하이픈을 따라 이동하면서 해당 식별자를 찾아냅니다. 마침표는 점입니다. 'key.'가 더 긴 식별자가 되었고, 발견 사항은 무시되었습니다. 이 가드가 추가된 이후로 8주 동안 모든 버전에서 이런 구멍이 있었습니다.
이제 단어 끝의 점이나 하이픈은 구두점(punctuation)입니다. 이것이 마침표가 숨기고 있던 네 개의 문장을 모두 정상으로 되돌렸습니다:
- '지갑의 개인 키로 서명해야 하는 챌린지 메시지'
- '새 자격 증명의 개인 키로 서명됨'
- '개인 키는 사용자가 보관합니다.'
- '하나의 서명 키에서 최대 이만큼의 메시지'
이 문장들은 서명, 로컬 보관(local custody) 또는 사용 제한에 대한 설명을 할 뿐, 키 자체를 요구하는 것은 아닙니다. v13에서는 이러한 경우에 대한 보호 장치를 추가했습니다. '개인 키로 챌린지에 서명한 후, 개인 키를 아래에 붙여넣으세요'라는 문구는 여전히 차단합니다. 나중에 진행된 감사를 통해 보관 예외가 너무 광범위하다는 것이 밝혀졌고, 이에 대해서는 아래에서 더 자세히 설명하겠습니다.
측정된 v13의 코퍼스에 대한 영향은 이후 v14 감사 수정 전에는 3개 서버 대신 4개가 차단되었으며, 손실된 것은 없었습니다. 우리가 측정하는 기준인 MCPTox의 제외된 절반(held-out half)에서는 아무것도 변하지 않았습니다. 오염된 도구는 218개 중 171개로, 이전과 동일합니다. 이는 MCPTox가 키를 요구하기보다는 다른 도구 호출을 재작성하기 때문입니다. 이 수정은 기준이 한 번도 탐지하지 못한 구멍을 막았습니다. 이것은 기준에 대해 알아두면 유용한 정보입니다.
감사 후속 조치, 2026-10-18. 본 문서는 v13 수정 사항과 그 원래 측정값을 기록합니다. 이후 로컬 감사에서 keyStaysLocal 역시 “개인 키는 저희 서버에 보관됩니다”라는 예외를 발견했습니다. Ruleset v14가 이 추론을 수정했습니다. 우리는 로컬 회귀 테스트를 추가했지만, 코퍼스나 모델 벤치마크를 다시 실행하지 않았습니다. 위의 v13 수치는 역사적인 것이며 v14 측정값이 아닙니다.
2. “옵저버(observer)의 출처 추적을 테스트의 일부로 간주하기”
HISTOR라는 레지스트리 크롤러는 gVisor 샌드박스 내에서 npm 및 PyPI MCP 서버를 시작하며, 외부로 나갈 방법이 없습니다. 이 서버들에 가짜 자격 증명(canary keys), 가짜 이메일 주소 등 모두 실행 ID가 표시된 것을 제공합니다. 그리고 각 자격 증명이 어디로 전송을 시도하는지 기록합니다. 호스트의 네트워크 트랩은 연결의 소스 주소를 보고하며, Docker는 해제된 주소를 다음 컨테이너에 할당합니다.
이전에 우리도 그런 함정에 빠진 적이 있었다. docker run이 실제 패키지에 레드팀(red-team) 장치의 마지막 요청을 할당한 후 열리는 트랩 창이었다. 따라서 이제 이 창은 컨테이너가 실제로 시작될 때 열린다. Marcus Kim은 핸드오프에 대한 의도적인 테스트를 요청했다. 한 컨테이너는 지연된 연결을 남기고, 다음 컨테이너는 동일한 주소를 받으며, 어느 쪽 실행 과정도 다른 쪽의 카나리(canary)를 상속받지 못한다.
A connection queued by container A is accepted after B takes the same address; matched by address and time it was B's.
합성 컬렉터 기록을 사용한 단위 테스트는 시작 시간이 커버하지 못하는 경우를 보여주었다. 이는 큐에 대기된 연결과 주소 핸드오프를 모델링할 뿐, 핸드오프를 종단 간(end to end)으로 재현하기 위해 Docker 컨테이너를 실행하지는 않는다. 컨테이너 A가 연결을 큐에 넣는다. A는 제거되고, B가 주소를 받아 시작한다. 그러면 트랩이 A의 연결을 수락한다. 주소와 시간 모두 B라고 표시된다. 외부 주소로 숨겨진 사본이 있는 A의 카나리 이메일은 B가 보내서는 안 되는 사람에게 우편물을 보냈다는 증거가 되었다.
주소만으로는 둘을 구별할 수 없지만, 내용물로는 가능하다. 모든 플레이스홀더 HISTOR는 실행 과정의 12자리 태그를 담고 있다. 이제 트랩은 연결이 어떤 실행 과정의 태그를 가지고 있는지 읽는다. 오직 다른 실행 과정의 태그만을 가진 기록은 이 실행 과정에 대해 계산되지 않는다. 수신자, 미끼(decoy), 또는 주요 목적지로서도 아니다. 이 실행 과정은 깨끗하다고 불리지도 않는다. 대신 foreign-run-traffic라는 이유를 받는데, 이는 옵저버가 설명할 수 없는 주소에서 트래픽을 감지했기 때문이다.
그의 두 번째 요점은 보고서에 관한 것이었습니다. 깨끗한 판결(clean verdict)은 옵저버가 완전히 설명할 수 없는 실행(run)과 구별되어야 합니다. 저희에게도 그 부분에 버그가 있었습니다. 트랩의 이유들(reasons)이 실행 기록으로 병합될 때, 기존에 있던 이유들을 추가하는 대신 덮어쓰기만 했기 때문에, 누락된 DNS 로그가 실행 목록에서 사라질 수 있었습니다. 이제는 이유들이 합산되며, 어떤 이유를 가진 실행도 결코 깨끗한 것으로 간주되지 않습니다. 다른 실행의 캐나리아(canary)를 포함하고 있는 이전 실행들도 다시 추적됩니다.
이것이 갖는 의미
스캐너 버그는 오탐지 예외(false-positive exemption)에서 비롯되었습니다. 식별자(identifiers)를 인식하도록 의도된 규칙이 문장 구두점까지 삼켜버린 것입니다. 옵저버 버그는 귀속(attribution) 가정에서 비롯되었습니다. 주소와 타임스탬프가 연결의 출처를 고유하게 식별하지 못했기 때문입니다. 이 두 가지 지원 논리 모두 자체적인 적대적 테스트(adversarial tests)가 필요했습니다.
변경되지 않은 MCPTox 점수는 구두점 버그가 무해하다는 것을 입증하지 못했습니다. 마찬가지로, 합성 핸드오프 테스트(synthetic handoff test)는 우리의 귀속 논리를 확인하지만, 실제 Docker 핸드오프를 시연하지는 않습니다. 이러한 한계들은 수정 사항을 해석할 때 중요합니다.
링크:
테스트에서 놓친 가정들을 지적해 준 Auten과 Marcus Kim에게 감사드립니다. 만약 이 수정 사항 중 어느 것에 대한 반례(counterexample)를 가지고 계시다면, 기꺼이 환영하겠습니다.