VSCode 버그를 통한 1-클릭 GitHub 토큰 탈취
(blog.ammaraskar.com)- github.dev는 github.com에서 전달받은 OAuth 토큰으로 브라우저 VSCode에서 파일 열람, PR, 커밋을 수행하며, 이 토큰이 특정 저장소로 제한되지 않아 사용자가 접근 가능한 저장소 전체를 읽고 쓸 수 있음
- VSCode webview는
vscode-webview://...iframe으로 격리하지만, 키보드 단축키 UX를 위해 webview의keydown을did-keydown메시지로 메인 창에 전달하면서 신뢰되지 않은 스크립트가 사용자 키 입력처럼 이벤트를 보낼 수 있음 - 임의 텍스트 입력은 HTML
<input>때문에 통하지 않지만, 기본 단축키Ctrl+Shift+A와 추천 확장 설치 알림, local workspace extensions 및 커스텀 키바인딩을 조합해 확장 설치 명령을 실행할 수 있음 - PoC는 Jupyter notebook의 마크다운 셀에서 JavaScript를 실행해 추천 확장 설치를 수락하고, 새 키바인딩으로 선택한 확장을 설치한 뒤 GitHub API 토큰과 비공개 저장소 목록을 표시함
- 데스크톱 VSCode도 같은 취약점이 있지만 공격자는 저장소 복제와 notebook 열기를 유도해야 하며, github.dev 사용자는 사이트 데이터를 지워 초기 확인 대화상자가 다시 나오게 하는 방어가 필요함
취약점 개요
- github.dev는 접근 가능한 GitHub 저장소 URL을
github.com에서github.dev로 바꾸거나 메뉴 항목을 클릭하면 브라우저에서 실행되는 경량 VSCode를 열어줌 - 이 브라우저 VSCode는 저장소 파일을 볼 수 있고, 비공개 저장소도 열 수 있으며, PR 전송과 커밋 생성도 가능함
- github.com은 사용자를 대신해 GitHub와 상호작용할 수 있는 OAuth 토큰을 github.dev로 POST하며, 이 토큰은 사용자가 상호작용한 특정 저장소로 제한되지 않음
- 공격자는 링크 클릭만으로 읽기·쓰기 권한이 있는 GitHub 토큰을 탈취할 수 있고, 대상에는 비공개 저장소도 들어감
Webview 격리와 키 입력 전달 문제
- VSCode webviews는 메인 VSCode 창과 다른 origin의
<iframe>을 사용해 JavaScript 실행을 격리함 - Jupyter notebook 출력은
vscode-webview://...origin의<iframe>에서 렌더링되고, 메인 Electron 창은vscode-file://...origin을 사용함 - 이 격리 덕분에 notebook이 HTML 표시나 JavaScript 기반 인터랙티브 위젯을 사용해도 iframe 안에서 Electron의 Node.js API나 VSCode API를 호출할 수 없음
- Markdown 미리보기처럼 메인 창과 webview가 협력해야 하는 기능은 Window.postMessage() API로 메시지를 주고받음
- VSCode는 webview 안에서 클릭한 상태에서도
Ctrl+Shift+P같은 단축키가 작동하도록did-keydown이벤트를 메인 창으로 전달함 - webview 안의 신뢰되지 않은 스크립트가
keydown이벤트를 직접 발생시켜 사용자가 키를 누른 것처럼 가장할 수 있음
공격 체인
Ctrl+Shift+P로 명령 팔레트를 열 수는 있지만, 명령 팔레트가 HTML<input>을 사용하기 때문에 임의 문자열을 입력하는 방식은 통하지 않음- 방향키와
Enter처럼keydown으로 처리되는 입력은 사용할 수 있고, VSCode의 기본 단축키 집합도 활용 가능함 Ctrl+Shift+A는 “Notifications: Accept Notification Primary Action” 기본 키바인딩이며, 마지막 VSCode 알림의 기본 버튼을 누름.vscode/extensions.json에 추천 확장을 넣으면 VSCode가 설치 알림을 띄우지만, VSCode 1.97의 publisher trust 시스템은 새 publisher 확장 설치 시 별도 신뢰 대화상자를 띄움Tab으로 버튼 이동은 가능해도 “Trust Publisher & Install” 버튼의Enter처리는 버튼 자체의keydown에 묶여 있어 이 경로만으로 설치를 완료하기 어려움- local workspace extensions는 신뢰된 workspace 안에서
.vscode/extensions에 있는 확장을 직접 설치할 수 있게 하며, github.dev/web workspace는 항상 신뢰된 상태임 - 로컬 workspace 확장을 직접 실행하려 하면 extension worker가
vscode-cdn.net에서 온 확장을 기대해 CSP 오류가 발생함 - 대신 로컬 workspace 확장의
package.json에 커스텀 키바인딩을 추가하고, 그 키바인딩이workbench.extensions.installExtension을skipPublisherTrust컨텍스트로 호출하게 만들 수 있음
PoC 동작과 영향
- 필요한 구성은 Jupyter notebook과 local workspace extension이 있는 저장소임
- notebook의 마크다운 셀은 이미지
onerror속성을 통해 JavaScript를 실행할 수 있음 - 페이로드는 VSCode가 추천 확장 설치 알림을 띄울 때까지 기다린 뒤
Ctrl+Shift+A이벤트를 보내 알림의 기본 동작을 수락함 - 이후 확장이 설치·활성화되고 커스텀 키바인딩이 들어올 때까지 기다린 뒤
Ctrl+F1이벤트로 선택한 확장 설치를 트리거함 - PoC로 설치된 확장은 GitHub API 토큰을 가져오고
https://api.github.com/user/repos를 조회해 접근 가능한 비공개 저장소를 얻은 뒤, 토큰과 저장소 목록을 정보 박스에 출력함 - PoC 실행 후에는 github.dev 데이터를 지우거나 PoC 확장을 제거해야 하며, 제거하지 않으면 모든 github.dev 페이지에서 따라다님
- 데스크톱 VSCode에도 같은 취약점이 있지만, 공격자는 피해자가 저장소를 clone하고 webview 스크립트 페이로드가 있는 notebook을 열도록 유도해야 함
- 피해자가 여는 webview에 다른 XSS가 있다면 데스크톱에서도 사실상 전체 원격 코드 실행으로 이어질 수 있음
방어와 완화 요소
- github.dev를 과거에 사용한 적이 없다면 웹사이트 진입 시 클릭해야 하는 대화상자가 하나 있어 공격 페이지에서 벗어날 기회가 생김
- github.dev의 쿠키와 로컬 사이트 데이터를 지우면 이 초기 대화상자가 다시 나타날 수 있음
- Chrome에서는 URL 바의 아이콘을 눌러 Cookies and site data > Manage on-device site data로 이동한 뒤 관련 도메인 데이터를 삭제할 수 있음
- 이미 github.dev 대화상자를 통과했고 브라우저 로컬 스토리지를 지우지 않았다면, github.dev에 CSRF 토큰 같은 보호가 없어 인터넷의 어떤 링크든 공격으로 리디렉션할 수 있음
- VSCode는 iframe 격리에만 의존하지 않고 엄격한 Content Security Policy와 DOMPurify를 함께 사용함
- 확장 페이지의 Markdown 미리보기에서
script-src 'none'을 사용해 임의 JavaScript 실행을 막기 때문에, 확장 링크만으로 데스크톱 1-클릭 RCE가 되는 더 큰 영향은 차단됨
공개 배경과 일정
- MSRC는 과거 VSCode 버그 보고를 조용히 수정하면서 크레딧을 주지 않았고 보안 영향 없음으로 표시했음
- 최근 Starlabs의 VSCode XSS 버그 보고도 부적격과 낮은 심각도로 표시됐음
- VSCode 팀에는 UI/UX와 보안 사이의 균형을 잡을 시간이 더 필요했을 수 있지만, 보안 연구자의 시간과 노력을 당연하게 여기면 안 된다는 이유로 전체 공개가 선택됨
- 2026년 6월 2일 게시 한 시간 전 GitHub 보안 쪽의 기존 연락처에 공개 예정이 전달됨
- 같은 날 취약점이 공개됐고 VSCode issue tracker에도 등록됨
댓글과 토론
Hacker News 의견들
-
좋은 정리였고, 크게 보면 웹에 임베드된 VSCode 편집기가 GitHub에 로그인되어 있다는 사실 자체가 아쉬움
심층 방어 여부와 별개로, 그 원죄 때문에 공격 표면이 크게 생김. 악성 NPM 패키지가 찾을 수 있도록 워크스테이션에 모든 권한을 가진 GitHub API 토큰을 평문으로 두는 것과 비슷함
이상적으로는 브라우저 IDE가 해당 저장소에 대한 pull/push만 가능한 임시 저장소별 권한 범위나 토큰으로 실행되고, github.com 웹 세션은 전혀 없었으면 좋겠음. 전체 GitHub 웹 UI가 필요하면 github.com으로 돌아가고, github.dev는 단일 저장소 서비스로 두는 식이 맞아 보임
다만 사용자에게 불편하고, 구현도 어렵고, github.dev 도구 전반에 역사적으로 박힌 가정일 가능성이 큼- Codespaces는 실제로 그렇게 함. 토큰은 활성화한 Codespace의 저장소에 대한 읽기/쓰기 권한만 가짐 [1]
github.dev에도 이 방식을 진지하게 고려해야 함
[1] https://orca.security/resources/blog/hacking-github-codespac... - 악성 NPM 패키지 문제는 점점 더 심해질 것 같음. 최근 OpenCode 같은 AI 실행 도구가 백그라운드에서 임의의 npm 패키지를 내려받고, 홈 디렉터리와 프로젝트 디렉터리 여기저기에 뿌리는 걸 봤는데 사용자에게 알리거나 묻지도 않았음
더 나쁜 건 개발자들조차 별로 신경 쓰지 않는 듯하다는 점임 - 자기 저장소를 열 때 로그인된 상태인 건 괜찮지만, 다른 계정의 저장소를 열 때는 절대 그러면 안 된다고 봄. 그리고 webview 키보드 단축키도 무해한 키 바인딩만 허용하고 어떤
keydown핸들러에도 전파되지 않게 고쳐야 함
데스크톱에서는 Electron이 직접 가로채도록 바꾸고 이 기능은 제거하는 편이 낫고, 웹에서는 기본값으로 비활성화하는 게 맞아 보임 - SSH 키와 GitHub deploy key를 쓰면 어느 정도 비슷하게 만들 수 있음. 보안성까지는 단언 못 하지만, 모든 저장소에 접근 가능한 GitHub 설정은 해본 적이 없음
다른 Git 호스팅에도 비슷한 기능이 있는지는 잘 모르겠음 - 해당 저장소에서 pull은 가능하되, push는 토큰이 아니라 사용자가 최종 push할 수 있는 스테이징 영역으로만 하게 하면 어떨까 싶음
솔직히 LLM 에이전트도 이렇게 해야 함. LLM이 직접 push하게 두는 건 무모해 보임
- Codespaces는 실제로 그렇게 함. 토큰은 활성화한 Codespace의 저장소에 대한 읽기/쓰기 권한만 가짐 [1]
-
이 공격이 특히 까다로운 이유는 VSCode 확장이 편집기 자체와 같은 신뢰 수준으로 실행되고, 대부분의 개발자가 권한을 검토하지 않은 확장을 수십 개씩 설치해 둔다는 점임
악성이거나 탈취된 확장이 GitHub 토큰을 조용히 유출하면 네트워크 감시 없이는 알아채기 어렵고, 확장은 격리된 프로필에서 실행해야 한다는 근거가 됨- 네트워크 감시가 있어도 GitHub 자체로 유출하면 막기 매우 어려움. SSL 가로채기와 매우 엄격한 URL 허용 목록이 없으면 특히 그렇다
가장 좋은 방법은 GitHub에서 벗어나 자체 호스팅 내부 GitLab/Forgejo로 옮기고 GitHub를 완전히 차단하는 것임
- 네트워크 감시가 있어도 GitHub 자체로 유출하면 막기 매우 어려움. SSL 가로채기와 매우 엄격한 URL 허용 목록이 없으면 특히 그렇다
-
최근에 비슷한 일을 겪었음. GitHub 토큰과 Cloudflare 토큰이 탈취됐음
보안을 진지하게 챙겨도 시간이 충분히 길면 결국 맞게 된다고 봄. 최선은 분리하고 피해 범위를 통제하는 것임
아무도, 아무것도 믿지 말고, OrbStack을 쓰고, 토큰은 언젠가 유출된다는 가정으로 항상 작업해야 함
작업 흐름이 완전히 끊겼지만, 다행히 토큰을 가져간 쪽은 스팸 봇에 가까웠던 듯함. 가짜 스팸 페이지를 잔뜩 만들고 암호화폐 채굴을 시도했음
가장 크게 남는 감정은 침해당했다는 느낌임. 다들 조심하길 바람- GitHub Pages 같은 페이지였는지 궁금함. 계정에 저장소가 생성됐던 건가? 토큰이 털렸다는 걸 어떻게 알아냈는지도 궁금함
-
MSRC에 VSCode 버그를 제보했을 때 조용히 고쳐버리는 끔찍한 경험이었다는 부분은 전형적인 MSRC임. 연구자들이 어차피 무료로 제보한다는 걸 알아낸 셈인데, 굳이 바꿀 이유가 없다고 보는 듯함
- MSRC가 버그를 고치는 건 아님
이 사례의 구체 사항은 모르지만, 예전에 Bountysource와 HackerOne을 통해 버그 바운티 프로그램을 운영한 적이 있음. 가끔 보안팀이 완전히 평가하기 전에 보고서가 개발팀으로 먼저 흘러가는 일이 생김
그 시점에 개발자가 조용히 고쳐버릴 수 있음. 보안 버그와 연관되면 자신에게 나쁘게 보이거나 승진 기회에 영향을 줄 수 있다는, 합리적이든 아니든, 우려 때문일 때도 있음. 결과적으로 보안팀이 재현하려고 할 때는 이미 취약점이 사라져 있음
MSRC 입장에서는 제공된 재현 절차가 더 이상 동작하지 않는 것만 보임. 내부 버그 이력이나 누군가 이미 패치했는지는 보이지 않음. 그래서 원래 발견이 정당했더라도 보고서는 무효로 닫힘 - 오랫동안 그게 현상 유지였는데, 성가신 보안 연구자들이 명성 대신 보상을 요구하기 시작함
- MSRC가 버그를 고치는 건 아님
-
이 익스플로잇에 쓴 시간을 사실상 기부해서 VS Code의 보안 대응 개선 필요성을 알린 셈이라 고마움. 그냥 포기할 수도 있었는데 여전히 돕고 있음
- 이 취약점을 팔거나 묵혀둘 생각은 없음. 다만 개념 증명을 만드는 데 몇 시간이 걸릴 수 있는데, 공급사가 조용히 패치만 하고 크레딧도 인정도 안 해주면 정말 기분이 나쁨
-
왜 더 많은 개발자가 Neovim을 시도하지 않는지 잘 이해가 안 됨
취향일 수도 있지만, 설치된 것과 실행 중인 것을 파악할 수 있는 작은 구성이 좋음. VSCode, 브라우저 IDE, 확장, 동기화, 토큰, 임의 플러그인이 섞이면 무엇이 무엇에 접근하는지 알기 어려워짐- 몇 년 전 VS Code를 그만두고 Neovim으로 옮겼음. VS Code가 기본 타입 정의가 없는 라이브러리를 위해 임의의 Python 패키지를 자동 설치한다는 걸 알게 된 뒤였음
Microsoft 공식 Python 확장의 기능이었고, 다른 면에서는 그나마 쓸 만한 유일한 확장이었지만, 내 프로젝트가 쓰는 버전과 다른 라이브러리 버전의 타입 정의를 설치했음. 검증되지 않은 서드파티 코드를 아무렇지 않게 실행하는 것처럼 보여 매우 불안했고, 설정으로 끌 수도 없어 보였음
“그 뒤로 뒤돌아보지 않았다”고 말하고 싶지만, 솔직히 지난 1~2년 동안 Neovim이 거의 업그레이드마다 내 설정을 정기적으로 깨뜨리기 시작했음. 언젠가 그럴 수도 있겠다는 낌새는 있었음. 엄밀히 말해 10년이 지났지만 nvim은 아직 첫 안정 버전을 내지 않았고, 그래서 불안정성을 탓할 수는 없지만 기억해둘 만함
다시 순정 Vim으로 돌아갈까 고민 중임. 편의 기능은 많이 잃겠지만, 일하는 도중 깨진 기능을 디버깅해야 하는 일은 줄었으면 좋겠음 - Helix가 꽤 마음에 듦. Neovim을 깊이 파보진 않았지만, Helix에는 Vim에서 늘 아쉬웠던 IDE 같은 기능이 꽤 잘 들어 있음
플러그인을 잔뜩 깔거나 SpaceVim 같은 걸 쓰지 않아도 됨. 한번 확인해보면 마음에 들 수도 있음 - 소프트웨어 습관을 바꾸게 만드는 건 꽤 어렵다는 걸 느낌. 배워야 할 단축키가 있고, 처음에는 느리게 느껴져서 “더 낫지 않다”는 감각이 강화됨
nvim에 익숙해지는 데 시간이 걸리지만, 익숙해지면 더 빠름. 그래도 많은 사람이 안락한 영역에 머무르는 이유는 설명됨
- 몇 년 전 VS Code를 그만두고 Neovim으로 옮겼음. VS Code가 기본 타입 정의가 없는 라이브러리를 위해 임의의 Python 패키지를 자동 설치한다는 걸 알게 된 뒤였음
-
공개 공개는 잘한 일임. MSRC에 불만을 가진 사람이 너무 많고, Nightmare Eclipse 상황처럼 이제 끓어넘치기 시작함
이런 공개들이 쌓이면 MSRC가 스스로를 돌아보고 자신들이 문제라는 걸 깨달을지도 모름. 그럴 가능성은 낮아 보이지만 기대는 해볼 수 있음- 이게 여전히 최선의 접근인지는 잘 모르겠음. 기존 XSS 제출과 비교했을 때 “낮음” 등급을 받을 거라고 예상해서 아예 제출도 해보지 않은 듯함
그래도 최소한 시도해보거나, 공개하기 여러 날 전에 알려줬어야 한다고 봄. 어떻게 될지는 모르는 일이니까
- 이게 여전히 최선의 접근인지는 잘 모르겠음. 기존 XSS 제출과 비교했을 때 “낮음” 등급을 받을 거라고 예상해서 아예 제출도 해보지 않은 듯함
-
글은 아주 좋았지만 마지막 부분에서 조금 헷갈렸음. 이해한 게 맞는지 확인하고 싶음
저자는 새 게시자 신뢰 시스템 때문에 단축키 트릭만으로 악성 확장을 직접 설치할 수는 없다고 했고, 게시자 검사가 없는 로컬 워크스페이스 확장으로 이를 우회할 수 있지만 CSP가 막는다고 했음
해결책은 “게시자 확인 없이 확장 설치” 단축키를 바인딩하는 로컬 워크스페이스 확장을 설치하는 것으로 보임
그래서 1) 확장이 두 개 필요하다는 뜻인지 궁금함. 첫 번째는 키 바인딩만 하는 로컬 확장이고, 두 번째는 실제 악성 확장이며 CSP 때문에 로컬일 필요도 없고 실제로 로컬일 수도 없는 것인지, 2) CSP는 로컬 확장의 JS만 막고package.json이나 단축키 추가 능력은 막지 않는 것인지 궁금함- 1과 2가 맞음. 개념 증명 저장소를 보면 됨: https://github.com/ammaraskar/github-dev-token-steal-poc/tre...
가장 직접적인 실행을 위해my-extension/extension.js를 넣어볼 수 있지만 CSP가 막음. 다만script-srcCSP로 스크립트만 막는 것이어서package.json을 가져오는 건 허용됨. 그래서 그걸 이용해 키 바인딩을 기여하게 됨
- 1과 2가 맞음. 개념 증명 저장소를 보면 됨: https://github.com/ammaraskar/github-dev-token-steal-poc/tre...
-
MSRC 상황은 정말 믿기 어려움
더 좋은 자료도 있겠지만 The Primeagen의 이 영상이 좋은 입문 자료라고 봄
https://www.youtube.com/watch?v=9kxx5xp5nTQ -
“이 동작을 허용하는 유일한 방법은 서로 다른 출처의 두 웹 페이지가
Window.postMessage()API로 협력하는 것”이라는 부분은 작은 nitpick이 있음
iframe이나 부모 창이location.anchor속성을 바꾸는 방식으로도 통신할 수 있음