技術 約9分で読めます

VS Code Remote-SSHで侵害済みSSH先からローカル端末へコマンドが届く

いけさん目次

TL;DR

何が起きてる 侵害済みSSH先の悪意ある拡張が、VS CodeやCursorのRemote-SSH経由でローカル端末を開きコマンドを送れる(Califの実証)。RCE=遠隔コード実行が、リモート側からローカル側へ抜ける

自分への影響 Remote-SSHを怪しいコードやAIエージェントの隔離箱として使っている人。信頼できないSSH先・共有サーバーに接続する人

対応 接続先をローカルと同等に信頼できる相手に限定。隔離目的ならVM・コンテナ・OSサンドボックスへ。CVEもパッチもない(Microsoftは仕様扱い)


VS Code Remote-SSHを「怪しいコードをリモート側に閉じ込める箱」として使っているなら、CalifのVibe Hacking実証でその前提が崩れる。
リモート側を取られた状態で、Remote-SSH経由のVS CodeやCursorはローカル端末を開かされ、そこへコマンド列を送られる。

MicrosoftのRemote-SSH Marketplaceページにも、侵害済みリモートがVS Code Remote接続を使ってローカルマシンでコードを実行できる、というセキュリティ上の注記(Security Note)が載っている(原文は “A compromised remote could use the VS Code Remote connection to execute code on your local machine”)。
今回の話は「まだ知られていなかったCVE」よりも、Remote-SSHの信頼モデルを具体的な手順で踏み抜いた実証に近い。
信頼境界(ここから先は信用しない、という設計上の線引き)が、ローカルとSSH先のあいだに引かれていないことが核心だ。

リモート側の拡張がローカル端末を開く

Califの説明では、攻撃の入口は侵害済みSSHサーバー上の悪意ある拡張機能だ。
開発者がそのサーバーへRemote-SSHで接続すると、リモート側の拡張はVS Codeのコマンドを呼び出せる。

使われるコマンドは2つある。
workbench.action.terminal.newLocal はローカル側の統合ターミナルを作る。
名前にLocalが入っている通り、SSH先のシェルではなく、開発者の手元のマシンで動くターミナルだ。

その後に workbench.action.terminal.sendSequence で文字列を送る。
VS Codeのターミナル機能としては、キー割り当てからEnterやエスケープシーケンスを送るための正規機能だ。
Califの説明では、改行文字(\n)を末尾に付けて送るとそのまま実行される。
ユーザーが手元でコマンドを打ってEnterを押したのと同じ結果になる。
どちらのコマンドも開発者の確認ダイアログを挟まない。リモート側から呼ぶだけでローカル端末が動く。

flowchart TD
    A["開発者がRemote-SSHで<br/>SSH先へ接続"] --> B["SSH先が侵害済み"]
    B --> C["リモート側の拡張が<br/>VS Codeコマンドを実行"]
    C --> D["newLocalで<br/>ローカル端末を作成"]
    D --> E["sendSequenceで<br/>コマンド文字列を送信"]
    E --> F["開発者端末で<br/>コマンド実行"]

    style F fill:#991b1b,color:#fff

Cyber Security Newsの記事はこれをRemote-SSHのRCE(Remote Code Execution、遠隔コード実行)として紹介している。
ただし、現時点で確認できる範囲ではCVE番号や修正版の案内ではなく、Remote-SSHが元から持つ「信頼したリモートとローカルを強くつなぐ」設計が焦点だ。

なぜリモート側の拡張がローカルで動けるのか

Remote-SSHを使うと、VS Codeは拡張機能を2箇所に分けて動かす。
この分割を知ると、リモート側のコードがローカル端末へ届く経路が見えてくる。

VS Codeは拡張機能を実行する場所を「拡張ホスト」と呼ぶ。Remote-SSH接続中は、ローカルとSSH先の両方に拡張ホストが立つ。
どちらで動くかは拡張機能側の extensionKind 設定で決まる。

種類extensionKind動く場所役割の例
UI拡張["ui"]開発者のローカルマシンテーマ、キーバインド、ローカルデバイス連携
Workspace拡張["workspace"]ワークスペースがある側(SSH先)言語サーバー、リンター、ファイル操作

UI拡張は「ローカルの資産・デバイス・低遅延が要るからUIの近くで動く必要がある」もの、Workspace拡張は「ワークスペースの中身にアクセスするから、ワークスペースのある場所で動く必要がある」ものとVS Codeの公式ドキュメントは定義している。
SSH先のリポジトリを開いて作業しているとき、その作業を支える拡張はSSH先で動いている。

この分割の上で、拡張から呼べるコマンドAPIが経路をつなぐ。
workbench.action.terminal.newLocal の名前通り、SSH先で動いている拡張でも、コマンドの実行先はローカル側になる。
リモートの拡張ホストとローカルの拡張ホストは1本の信頼された経路でつながっていて、リモート側からローカル側のコマンドを呼べる。
SSH先を取られた攻撃者は、その経路ごと手に入れる。

AIエージェントの退避先として使うと危ない

この話が問題になるのは、AIコーディングエージェントをローカルから離したい場面だ。
「CursorやVS CodeでSSH先に入り、AIエージェントはリモートで走らせる。だからローカル端末は守られる」という運用は、Remote-SSHのセキュリティ注記と反対の前提に立つ。

リモート側のAIエージェントが暴走するだけなら、被害はSSH先のファイルシステムに留まるように見える。
でもRemote-SSH接続中は、SSH先がVS Code Serverや拡張機能を通じてローカル側の機能へ触れる経路を持つ。
Califの実証は、その経路からローカル端末の作成と入力送信へ到達している。
SSH先が信頼できる運用環境なら便利な経路だが、不明なリポジトリやAIエージェント実験用の隔離箱として使うと、ローカル端末へ届く経路が残ったままになる。

Cursorにも同じ前提が乗る

CalifはCursor固有の問題ではなく、CursorがVS CodeのRemote-SSH系の仕組みを継承している点を指摘している。
Cursor 3のようにIDEがエージェントの実行環境へ寄っていくと、ローカルとリモートの境界はさらに曖昧になる。
クラウドやSSH先で動かしているつもりでも、IDEがローカル端末、認証情報、ファイル、ブラウザへ接続経路を作っている。

Cursorでは別件として、CVE-2026-26268でGit hooksからサンドボックス外RCEへつながる問題も出ていた。
あちらはGit hooks、今回はRemote-SSHのローカル端末作成だが、構図は近い。
開発ツールの正規機能が、AIエージェントやリモート開発の自動実行と組み合わさると、ユーザーの確認を通らずにホスト側へ届く。

同じCalifが報告したiTerm2のCVE-2026-41253も、SSH先の出力が手元の端末で実行される系統の話だった。
SSH接続そのものを「向こう側で完結している」と思い込む前提を、別々の角度から崩している。

Microsoftはこれを仕様として扱う

Califによると、この挙動をめぐるGitHub issueでMicrosoftは拡張機能の安全性を高める変更は行わない方針を示している。
つまりパッチ待ちではなく、Remote-SSHの信頼モデルを前提に運用側で対処する話になる。
記事執筆時点でCVE番号は確認できない。修正版を待つ種類の問題ではない。

VS Codeには信頼できないリポジトリを制限モードで開くWorkspace Trustという仕組みがあるが、これはRemote-SSHのこの経路を守る設計ではない。
Workspace Trustが見るのはワークスペースのファイル内容の信頼であって、接続先のSSHホストそのものを信頼するかどうかの判定ではない。
Microsoftのトラッカーでも、Remote-SSH接続のワークスペースにWorkspace Trustをうまく適用できないという議論が以前から残っている。

確認する場所

すでにRemote-SSHで不明なサーバーや共有サーバーへ接続していたなら、ローカル端末側の履歴を見る。
macOSならシェル履歴、WindowsならPowerShell履歴、VS Codeの統合ターミナルのログ、EDRのプロセス実行履歴が手掛かりになる。
curlosascript、PowerShellのダウンロード実行、SSH鍵やクラウド認証情報を読むコマンドがあれば、該当時間帯のリモート接続先と突き合わせる。

VS CodeやCursorのリモート側拡張も見る。
SSH先の ~/.vscode-server/extensions、Cursorなら ~/.cursor-server 配下に、知らない拡張や更新時刻の新しいファイルが残っていないか確認する。
ただ、Califも書いている通り、SSH先を完全に取られている場合、この確認だけでは痕跡を拾えないケースが残る(侵害したサーバー側のファイルは攻撃者が後から書き換えられる)。
侵害後に痕跡を消される前提で、ローカル端末側の実行履歴を優先して追う。

ローカル側で当たる場所を具体的に並べると次の通り。

OS見る場所拾いたいもの
macOSシェル履歴(~/.zsh_history 等)、EDRのプロセス実行ログcurlosascript、SSH鍵・クラウド認証情報の読み取り
WindowsPowerShell履歴、EDRのプロセス実行ログダウンロード実行、認証情報・トークンの読み取り
共通VS Codeの統合ターミナルのログ、シェルの起動時刻該当時間帯にRemote-SSHで接続していた相手

身に覚えのないコマンドが出たら、その時刻に接続していたSSH先と突き合わせる。

隔離が目的なら接続の向きを変える

Remote-SSHは「自分の管理下にあるサーバーを、手元のIDEから便利に触る」用途に向いている。
接続先がローカルと同じくらい信頼できる相手なら、ローカル端末へつながる経路があっても困らない。

問題は隔離目的のときだ。不明なリポジトリやAIエージェント実験用の隔離箱としてRemote-SSHを使うと、Califの実証の通りローカル端末へ届く経路が残る。
隔離が目的なら、IDEからSSHで入る形ではなく、実行環境そのものを分ける。

  • VM・コンテナ・OSサンドボックスの中でIDEごと動かし、ローカルの本番環境から切り離す
  • どうしてもRemote-SSHを使うなら、接続先を使い捨てにして認証情報やSSH鍵を置かない
  • リモートで走らせるAIエージェントにも、ローカルと同じ警戒度で接する

既存記事のAIエージェントのローカル隔離実行で書いた sandbox-exec やWindowsサンドボックスは、エージェント自身をローカルで縛る方向の話だった。
Remote-SSHで遠隔へ逃がす方向とは別物で、SSH先を信頼していい相手かどうかが前提から問われる。

参考