当社のAIエンジンNeutronが、UCバークレーのCyberGymベンチマークで97.1%のスコアを記録しました。 詳細を見る

ディープダイブ

ディープダイブ

OpenHands Software Agent SDKにおける認証不要のサンドボックスRCE(CVE-2026-112346)

OpenHands Software Agent SDKは、サンドボックスのエージェントポートをすべてのインターフェースで公開し、認証もデフォルトで無効なため、rootとしてコマンドを実行できてしまいます。1.52.0で修正されたのはDockerランチャーのみで、Apptainerは未修正です。

エグゼクティブサマリー(TL;DR)

  • OpenHands Software Agent SDKはワークスペース内でエージェントを動かします。エージェントサーバーはコマンドやファイル操作を実行するHTTPおよびWebSocketサービスです。ワークスペースランチャーであるDockerWorkspaceまたはApptainerWorkspaceがこのサービスを起動します。OpenHands Software Agent SDKのDockerランチャーはサンドボックスのエージェントサーバーのポートをホストのすべてのインターフェースで公開し、Apptainerランチャーはサーバーをホスト上で直接0.0.0.0にバインドします。セッションAPIキーのリストが空の場合はデフォルトで認証が無効になります。ポートに到達できるホストであれば、認証なしのHTTPまたはWebSocketリクエストを送信して、サンドボックス内でrootとして任意のシェルコマンドを実行できます。これには、共有Dockerネットワーク上のコンテナ、LAN上のマシン、ホストが公開されている場合はインターネット上のクライアントが含まれます。
  • 1.52.0より前のすべてのopenhands-sdkリリースが影響を受けます。執筆時点で、このパッケージのPyPIでの直近1週間のダウンロード数は604,050件でした。
  • CVEを割り当てて公開したVulnCheckは、重大度をクリティカル、CVSS v3.1で9.8、CVSS v4.0で9.3、CWE-1327(制限のないIPアドレスへのバインド)と評価し、発見者としてmil4nをクレジットしています。悪用は確認されておらず、公開された概念実証(PoC)も存在しません。
  • Dockerの場合は、2026年10月5日にリリースされたopenhands-sdk 1.52.0以降に更新し、すべてのサンドボックスコンテナを再作成して、ポートが127.0.0.1のみで公開されるようになったことを確認してください。古いデプロイでは、ポートへのネットワークアクセスを遮断してください。
  • 1.52.0はApptainerには何も変更せず、引き続き0.0.0.0にバインドします。SESSION_API_KEYを設定し(OH_SESSION_API_KEYS_0は未設定のままにするか、両方に同じ値を設定します)、ポートを信頼できないネットワークから切り離してください。1.52.0より前のDockerでは、キーを設定するとサーバーは保護されますがSDKのセッションが動作しなくなります。認証付きの利用を復旧するにはアップグレードしてください。

自社は影響を受けるか

openhands-sdkを1.52.0未満のDockerWorkspaceランチャー、またはApptainerWorkspaceランチャーで使用し、セッションAPIキーを設定しておらず、完全には信頼できないネットワークからサンドボックスのポートに到達できる場合、影響を受けます。アドバイザリの影響範囲は1.52.0より前までですが、Apptainerランチャーは1.52.0でも0.0.0.0にバインドします。3つの条件すべてが必要です。

  • 自社のコードまたはプラットフォームが、OpenHands Software Agent SDK(パッケージopenhands-sdk)を、1.52.0より古いバージョンのDockerWorkspaceランチャー、または1.52.0でも変更されていないApptainerWorkspaceランチャーで動作させている。
  • セッションAPIキーが設定されていない。これはデフォルトの状態で、SDKを起動する環境にSESSION_API_KEYもOH_SESSION_API_KEYS_0も設定されていないため、サーバーのsession_api_keysリストが空になり、認証が無効になっています。
  • 公開されたポートに、完全には信頼できない相手が到達できる。修正前のランチャーはポートをホストのすべてのインターフェースで公開するため、共有Dockerブリッジネットワーク上の他のコンテナ、ホストのLAN上のあらゆるもの、さらにホストがパブリックアドレスを持ち、遮断するファイアウォールがない場合はインターネット全体が含まれます。
パッケージ 影響を受けるバージョン 修正済みバージョン
openhands-sdk(PyPI) 1.52.0より前 1.52.0(2026年10月5日リリース)
OpenHands Software Agent SDK DockerWorkspaceランチャー 1.52.0より前 1.52.0:127.0.0.1で公開し、クライアントが設定されたセッションキーを読み取る
OpenHands Software Agent SDK ApptainerWorkspaceランチャー アドバイザリでは1.52.0より前だが、1.52.0も該当 1.52.0で変わるのはDockerでの公開のみで、v1.52.0でもApptainerは0.0.0.0にバインドする。セッションキーを設定し、ポートを分離すること(修正のセクションを参照)

実行しているものと、ポートの公開先を確認します。

python -c "from importlib.metadata import version; print(version('openhands-sdk'))" 2>/dev/null || echo 'openhands-sdk not installed in this interpreter'
docker ps --format '{{.Names}}  {{.Ports}}' | grep -E 'agent-server|->8000/tcp'
# Run where the SDK starts; report presence without printing secret values.
python - <<'PYKEYS'
import os
for name in ("SESSION_API_KEY", "OH_SESSION_API_KEYS_0"):
    print(name + ": " + ("set" if os.environ.get(name) else "unset or empty"))
PYKEYS

docker psの出力では、修正前のサンドボックスは0.0.0.0:31245->8000/tcpのような公開行を表示します。これはすべてのインターフェースを意味し、修正済みのランチャーでは127.0.0.1:31245->8000/tcpと表示されます。ランチャーのDocker runコマンドは、各コンテナにagent-server-<uuid>という名前を付けます。

修正方法

DockerWorkspace:SDKを更新してから、すべてのサンドボックスコンテナを再作成してください。実行中のコンテナは、再作成されるまで古いポートマッピングを保持するためです。

python -m pip install --upgrade 'openhands-sdk>=1.52.0'

次に、実行中の各サンドボックスセッションを終了して新しいセッションを開始し、更新されたランチャーが新しいポートマッピングで新しいコンテナを作成するようにします(単なるdocker restartでは古いマッピングが残ります)。そのうえで、docker psで新しい公開アドレスを確認してください。修正済みのDockerランチャーのクライアントは、SDKプロセスの環境からOH_SESSION_API_KEYS_0を読み取り、設定されていなければSESSION_API_KEYにフォールバックします。これはサーバーのキー選択と一致するため、SDKのリクエストには設定されたキーが付与されます。

ApptainerWorkspace:1.52.0ではランチャーの0.0.0.0へのバインドは変わらないため、アップグレードだけではポートは公開されたままです。以下の説明に従って、セッションキーを設定し、ポートを隔離してください。

すぐに更新できない場合、またはApptainerを使用している場合は、ネットワークアクセスを制限し、認証を設定してください。ランチャーごとの制約は次のとおりです。

  • ポートを遮断する:ホストのファイアウォールルール、またはサンドボックスのポートへのローカル以外のトラフィックを破棄するDockerのDOCKER-USERチェーンのルールでリモートからの経路は閉じられます。ただし、ポートはすべてのインターフェースにバインドされたままであり、引き続き到達できるプロセスやコンテナは認証なしのAPIを完全に利用できます。
  • 認証を設定する:SDKプロセスを起動する環境で、起動前にexport SESSION_API_KEY="$(openssl rand -hex 32)"を実行します。ランチャーがキーを転送し、サーバーが適用します。すでに実行中のサンドボックスセッションはキーのないサーバーを保持するため、新しいセッションを開始してください。
  • Apptainer:クライアントは、v1.52.0を含めてSESSION_API_KEYを読み取ります。この変数を設定してOH_SESSION_API_KEYS_0は未設定のままにするか、両方に同じ値を設定し、クライアントとサーバーの設定を一致させてください。
  • 1.52.0より前のDocker:クライアントはキーをNoneにハードコードしています。どちらのキー変数を設定してもサーバーは保護されますが、SDKのセッションが動作しなくなります。一時的な封じ込めとして使用し、セッションを復旧するにはアップグレードしてください。

根本原因は何か

ランチャーはサンドボックスのポートをホストのすべてのインターフェースで公開し、セッションキーを設定していませんでした。さらに、キーが未設定の場合はループバックにバインドするというエージェントサーバー自身の安全なデフォルトを、明示的に上書きしていました。

脆弱なコードは、バージョン1.51.0のSDKにあるopenhands-workspace/openhands/workspace/docker/workspace.pyです。ランチャーは、サンドボックスとそのエージェントサーバーを起動するDocker runコマンドを組み立てます。

ports = ["-p", f"{self.host_port}:8000"]        # line 218
if self.extra_ports:
    ports += [
        "-p",
        f"{self.host_port + 1}:8001",  # VSCode   # line 222
    ]

Dockerの-pフラグは、コンテナのポートをホスト上で公開します。この例のように指定にホストIPが含まれない場合、公開アドレスは0.0.0.0、つまりホストが持つすべてのインターフェースになります。extra_portsを有効にした場合(デフォルトでは無効)、ランチャーはVS Codeのポート8001もhost_port + 1でホストIPなしに公開していたため、この追加のポートもすべてのインターフェース上にありました。同じrunコマンドのさらに後で、ランチャーはエージェントサーバーに待ち受けアドレスを明示的に渡します。

run_cmd = [
    "docker", "run", "-d", "--platform", self.platform, "--rm",
    ...
    image,
    "--host",
    "0.0.0.0",                                    # line 248-250
    "--port",
    "8000",
]

コンテナ内では、ポートフォワーディングのために0.0.0.0へのバインドが必要です。露出の範囲を決めるのはホスト側の公開先であり、それがすべてのインターフェースになっています。もう半分は認証です。コンテナの起動後、ランチャーはワークスペース用にSDK自身のクライアントを設定します。

if not self.host:
    object.__setattr__(self, "host", f"http://127.0.0.1:{self.host_port}")
object.__setattr__(self, "api_key", None)          # line 272

クライアントキー:DockerクライアントのキーはNoneにハードコードされています。これとは別に、ランチャーは設定済みのセッションキーの環境変数をコンテナに転送しますが、それを読むのはサーバーだけで、SDK自身のクライアントがキーを取得することはありません。

転送される設定:Dockerランチャー自身のforward_envフィールドの説明には、これらの変数により、サーバーが0.0.0.0で待ち受けている場合にリクエストを認証できると説明されています。

サーバーのデフォルト:キー変数が設定されていない場合、サーバーのsession_api_keysリストは空になります。CVEレコードは、このデフォルトによって認証が無効になると特定しています。

エージェントサーバーの安全なバインドのデフォルトは、2026年7月に導入されました。プルリクエスト#4180「fix(agent-server): default bind host to loopback without a session API key」により、キーのないサーバーはループバックにバインドするようになりました。ランチャーの明示的な--host 0.0.0.0がこのデフォルトを上書きするため、修正はサーバーではなくランチャーを変更する必要がありました。

どのように悪用されるか

ポートに到達できるクライアントは、認証なしの通常のAPIリクエストだけでサンドボックス内でrootとしてシェルコマンドを実行できます。 エクスプロイトコードは公開されていません。ペイロードを使わずに露出を確認するには、確認したいネットワーク上の位置からcurl -s -o /dev/null -w '%{http_code}\n' http://<sandbox-host>:<port>/api/conversations/countを実行します。200はキーなしでAPIが応答すること、401はセッションキーが適用されていること、000はその位置からポートに到達できないことを意味します。CVEレコードとソースから読み取れる攻撃経路は、4つのステップからなります。

  1. 攻撃者が公開ポートでホストに到達できる。たとえば、サンドボックスのホストと同じDockerブリッジネットワーク上のコンテナ、LAN上のホスト、マシンが公開されフィルタリングされていない場合はインターネット上の任意のホストです。
  2. 攻撃者がエージェントサーバーのAPIにHTTPまたはWebSocketリクエストを送信する。サーバーは空のsession_api_keysリストで起動されているため、キーは不要です。
  3. サーバーが要求されたシェルコマンドをサンドボックスコンテナ内でrootとして実行し、コマンドの出力をそのまま返す
  4. コマンド実行にとどまらず、同じAPIはマウントされたワークスペースディレクトリとシークレットストアにも到達する。CVEレコードによると、シークレットは平文で読み取られます。

このチェーンのどこにも、リクエスト自体の脆弱性は必要ありません。APIはエージェントが使うために設計されたSDK自身のものであり、単に誰からの接続でも待ち受けているだけです。

攻撃はどのように進むか

誤って公開されたポートへの認証なしのリクエストが、ワークスペースとシークレットストアが接続されたエージェント自身のサンドボックス内で、rootとしてのコマンド実行につながります。

flowchart LR A["ネットワーク上の攻撃者
LAN、コンテナ、インターネット"] --> B["ネットワークから到達可能なエージェントポート
Docker: ホストIPなしの-p
Apptainer: --host 0.0.0.0"] B --> C["session_api_keysリストが
空のエージェントサーバー"] C --> D["認証なしのHTTPまたは
WebSocketリクエストを受理"] D --> E["シェルコマンドを
サンドボックス内でrootとして実行"] E --> F["ワークスペースのファイルと
平文のシークレットストアを読み取り"] style B fill:#f8d7da style C fill:#f8d7da

攻撃者は何ができるか

影響範囲はサンドボックスコンテナと、そこにマウントされたデータです。CVEレコードによると、攻撃者はサンドボックスコンテナ内でrootとして任意のシェルコマンドを実行でき、エージェントが操作するマウント済みワークスペースに完全にアクセスし、SDKがセッション用に用意するシークレットストアを平文で読み取れます。これはエージェントの作業対象一式であり、エージェントが編集するリポジトリやディレクトリ、シークレットストア内の認証情報、そのほかセッションがマウントしたあらゆるものが含まれます。成立には次の2つの条件が必要です。

  • 公開ポートへのネットワーク到達性。修正前のランチャーはすべてのインターフェースでこれを与えます。認証、ユーザーの操作、ネットワーク上の位置以外の権限はいずれも不要です。
  • セッションキーを一度も設定していないデプロイ。これはSDKのデフォルトです。

修正はなぜ有効か

修正はコミット7b66584で、2026年10月3日にプルリクエスト#5209(「fix(workspace): publish Docker ports on loopback」)としてマージされました。10月5日にv1.52.0でリリースされ、Dockerの公開アドレスとクライアントのキー参照の両方に対処しています。

-        ports = ["-p", f"{self.host_port}:8000"]
+        ports = ["-p", f"127.0.0.1:{self.host_port}:8000"]
         if self.extra_ports:
             ports += [
                 "-p",
-                f"{self.host_port + 1}:8001",  # VSCode
+                f"127.0.0.1:{self.host_port + 1}:8001",  # VSCode
-        object.__setattr__(self, "api_key", None)
+        session_api_key = os.environ.get(
+            "OH_SESSION_API_KEYS_0", os.environ.get("SESSION_API_KEY")
+        )
+        object.__setattr__(self, "api_key", session_api_key)

公開先はループバックのみになったため、ネットワーク上の他のホストはポートに到達できず、VS Codeのポートも同じルールに従います。これがトレードオフです。別のマシン上のクライアントは、公開されたポートに直接接続できなくなります。プルリクエストはこの変更を、SDK自身のクライアントがすでに127.0.0.1経由で接続していることから、デフォルトのネットワーク動作をそのマシン内に留めるものとして説明しています。コンテナ内のサーバーは引き続き0.0.0.0にバインドしており、修正に含まれる回帰テストもそれを検証しています。コンテナ内部でのバインドはDockerのフォワーディングに必要なものであり、境界となるのはホスト側の公開アドレスです。

クライアントはサーバーが受け付けるのと同じキーを解決するようになり、現行のOH_SESSION_API_KEYS_0変数を優先し、従来のSESSION_API_KEYをフォールバックとして使います。これにより、設定したキーはサーバー側の受信チェックだけでなく、双方向の認証に使われます。

修正が変更するのはDockerランチャーで、そのテストはループバックでの公開とクライアントのキー参照をカバーしています。アドバイザリはApptainerにも言及していますが、そのランチャーはv1.52.0でも--host 0.0.0.0を渡しています。

Apptainerについては、修正は何も変更しません。ランチャーは1.52.0でも以前と同様に、エージェントサーバーをホスト上で直接--host 0.0.0.0で実行するため、ポートはすべてのインターフェースで待ち受けます。封じ込め策は修正のセクションにあるもの、つまりセッションキーとネットワークの分離です。サーバーはワークスペースのhost_portで待ち受けます。これは指定しない限り30000から39999の間の空きポートです。ホストでss -ltn | grep ':<port> 'を実行して0.0.0.0で待ち受けているかを確認し、続いて到達させたくないマシンからnc -vz <host> <port>を実行してください。接続に成功した場合はポートが露出しており、保護しているのはファイアウォールとキーだけです。

侵害されたかどうかを確認する方法

この脆弱性について侵害の痕跡(IOC)は公開されておらず、悪用も確認されていないため、grepで探すべきIOCのリストはありません。1.52.0より前のサンドボックスを到達可能なポートで実行していたデプロイで確認できることは、次のとおりです。

  • ポートマッピング(docker ps --format '{{.Names}} {{.Ports}}'):修正前のサンドボックスでは公開行に0.0.0.0:が表示され、ポートがすべてのインターフェースで公開されていたことが確認できます。これは露出の条件の半分にすぎず、信頼できない相手が到達できたかどうかは、ファイアウォールのルールとネットワーク経路で決まります。
  • サンドボックスコンテナのログ:エージェントサーバーは処理したリクエストをログに記録するため、自社のエージェントセッションの活動に対応しないHTTPまたはWebSocketリクエストを探します。
  • シークレットストアの内容のうち、サンドボックスが到達できたもの:露出したサンドボックスに用意されたシークレットはすべて漏えいしたものとして扱い、ローテーションしたうえで、クリーンなイメージからコンテナを再構築します。

露出の条件が一度も成立していなかった場合、つまりポートがファイアウォールで遮断されていたか、ホストに信頼できないネットワーク経路がなかった場合、実際のリスクはほぼなく、通常のスケジュールでパッチを適用すれば十分です。

タイムライン

  • 2026-07-23:プルリクエスト#4180で、エージェントサーバーに安全なデフォルト(セッションAPIキーが未設定の場合はループバックにバインド)が導入される
  • 2026-10-03:プルリクエスト#5209「fix(workspace): publish Docker ports on loopback」が、コミット7b66584としてマージされる
  • 2026-10-05:v1.52.0がリリースされる。変更履歴は修正をタイトルで記載するのみでセキュリティ上の位置付けはなく、アドバイザリも公開されない。
  • 2026-10-26:VulnCheckがCVE-2026-112346を公開。CVSS v3.1で9.8、v4.0で9.3、CWE-1327、発見者はmil4n、影響範囲は1.52.0より前。
  • 2026-10-26:GitHubアドバイザリデータベースが、このレコードをGHSA-rp7h-crm7-h5qjとして同期
  • 2026-10-27:本記事を公開

よくある質問(FAQ)

OpenHandsのメインプラットフォームにも同じ欠陥はあるか

アドバイザリが対象としているのは、SDKのローカルセッションが使用するDockerWorkspaceとApptainerWorkspaceランチャーです。ホスティング版のOpenHandsプラットフォームや他の製品がこれらのランチャーを組み込んでいるかどうかはアドバイザリでは明らかにされていないため、OpenHandsベースのデプロイを運用している場合は同じ確認を行ってください。確認するのは、インストールされているSDKのバージョンと、agent-serverコンテナの公開アドレスです。探すべきパターンは、0.0.0.0でポート8000を公開しているagent-server-*という名前のコンテナです。

v1.52.0はなぜセキュリティアドバイザリなしでリリースされたのか

v1.52.0の変更履歴は、プルリクエストをそのタイトル「fix(workspace): publish Docker ports on loopback」で記載しているだけでセキュリティ上の位置付けはなく、リリースにはアドバイザリも伴いませんでした。そのため、10月26日にVulnCheckのCVEレコードが公開されるまで依存関係スキャナーには警告する材料がなく、その間はCVEフィードだけでなくバージョンの確認が信頼できるシグナルでした。

セッションキーだけで十分に保護できるか

会話、コマンド実行、ファイルアクセスの保護されたルートでは、サーバーがセッションキーを確認するため、認証なしでコマンドを実行する経路は閉じられます。公開されているサーバー情報のルートでは、認証の有効性を正しく検証できません。しかし、ポートの公開先は変わりません。1.52.0より前のDockerランチャーは引き続きすべてのインターフェースで公開し、自身のクライアントはキーを送信できないため、セッションが動作しなくなります。1.52.0より前のApptainerランチャーは、クライアントがSESSION_API_KEYを読み取るため、それを設定しても動作し続けます。Dockerでは、アップグレードによってループバックでの公開が加わります。Apptainerでは、認証に加えてポートをネットワーク上で分離してください。1.52.0ではそのバインドアドレスは変わりません。

実行中のコンテナを再作成する必要があるか

はい。ポートマッピングはコンテナの作成時に設定されるため、ホスト上のSDKをアップグレードした後も、すでに実行中のサンドボックスは0.0.0.0での公開を保持します。すべてのサンドボックスを停止して再作成し、docker psで新しいマッピングを確認してください。

なぜ修正後3週間もアドバイザリがなかったのか

CVEレコード、GHSAエントリ、データベースのマッピングはいずれも、修正版のリリースから3週間後の10月26日付です。すぐにアップグレードしたデプロイは理由を知らないまま安全になり、バージョンを固定しているデプロイにはその間、警告の根拠となるアドバイザリがありませんでした。これは、エージェントのインフラでバージョン管理を徹底すべきだという標準的な論拠です。ネットワークポートにバインドするサンドボックスのコンポーネントは、CVE単位だけでなくリリース単位で追跡する価値があります。

1.52.0未満のopenhands-sdkはどこに残っているか

自社のリポジトリとコンテナイメージに対してOstorlab SCAを実行し、1.52.0未満のopenhands-sdkをいまだに同梱しているすべてのプロジェクトとリリースを洗い出してください。

参考文献

  • CVE-2026-112346のレコード。2026年10月26日にCNAとしてVulnCheckが公開。CVSS v3.1の9.8およびv4.0の9.3のベクトル、CWE-1327、1.52.0より前という影響範囲、両ランチャーの説明を含む:https://www.cve.org/CVERecord?id=CVE-2026-112346(2026年10月28日確認)
  • GHSA-rp7h-crm7-h5qj。CVEレコードから同期されたGitHubアドバイザリデータベースのエントリ:https://github.com/advisories/GHSA-rp7h-crm7-h5qj(2026年10月27日確認)
  • VulnCheckのアドバイザリ「OpenHands Software Agent SDK < 1.52.0 Unauthenticated Sandbox Agent Server RCE」。重大度はクリティカル、発見者はmil4n:https://www.vulncheck.com/advisories/openhands-software-agent-sdk-unauthenticated-sandbox-agent-server-rce(2026年10月27日確認)
  • 修正プルリクエスト#5209「fix(workspace): publish Docker ports on loopback」。2026年10月3日にマージ。説明にクライアントへのキー伝播の根拠を記載:https://github.com/OpenHands/software-agent-sdk/pull/5209(2026年10月27日確認)
  • 修正コミット7b66584ff8899144eb4ce679c2811861342af9de。ループバックでの公開とセッションキー解決のdiff:https://github.com/OpenHands/software-agent-sdk/commit/7b66584ff8899144eb4ce679c2811861342af9de(2026年10月27日確認)
  • v1.52.0リリース。2026年10月5日公開、修正をセキュリティ上の位置付けなしに記載:https://github.com/OpenHands/software-agent-sdk/releases/tag/v1.52.0(2026年10月27日確認)
  • v1.51.0の脆弱なソース。openhands-workspace/openhands/workspace/docker/workspace.pyの89〜95行目(forward_env)、218行目、222行目、248〜250行目、272行目:https://github.com/OpenHands/software-agent-sdk/blob/v1.51.0/openhands-workspace/openhands/workspace/docker/workspace.py(2026年10月27日確認)
  • エージェントサーバーの安全なデフォルト。プルリクエスト#4180「fix(agent-server): default bind host to loopback without a session API key」、2026年7月23日にマージ:https://github.com/OpenHands/software-agent-sdk/pull/4180(2026年10月27日確認)
  • openhands-sdkのPyPIダウンロード統計。確認した週のダウンロード数は604,050件:https://pypistats.org/packages/openhands-sdk(2026年10月27日確認)
  • v1.52.0のApptainerランチャー。0.0.0.0へのバインドとSESSION_API_KEYによるクライアントのキー参照が残っている:https://github.com/OpenHands/software-agent-sdk/blob/v1.52.0/openhands-workspace/openhands/workspace/apptainer/workspace.py(2026年10月28日確認)