Browser use SDKのURLポリシーとegress制限 — 安全に動かす6手順
SDKのブラウザ使用ツールセットはURLを検査しません。URLポリシー、ドライバの傍受、コンテナのegress、ファイルの閉じ込め、confirm、ホスト隔離の6手順を実装例つきで示します。
PythonとTypeScriptのSDKには、browser use toolを動かすためのクラスがあります。開発者はそのクラスを継承し、navigate や left_click といったメンバーごとのメソッドを自分のブラウザ自動化に対して書きます。SDKは呼び出しの振り分け、渡したポリシーの実行、承認コールバックの呼び出し、tool_result の組み立てを担います。
このクラスには、ブラウザも既製のドライバもURLポリシーも付いていません。URLポリシーを渡さなければ、SDKはURLを1件も検査せず、APIもClaudeが開くURLを絞りません。安全に動かす作業は、ほとんどが開発者側の設計になります。
SDKが検査しないものと、6手順の全体像
ポリシー無しの構成では、ページ内のリンクやリダイレクトで次の宛先に到達できます。
- ループバック(
localhost、127.0.0.1) - クラウドのメタデータアドレス
169.254.169.254を含むリンクローカルアドレス - プライベートネットワークの範囲
さらに file: URLならブラウザホスト上のファイルを読めます。javascript: URLは javascript_exec が無効でも現在のページでスクリプトを走らせます。どちらもコンテナのegressルールでは止まりません。
ドキュメントが挙げる対策は6つで、SDKが肩代わりするのは一部だけです。
安全に動かす6手順
- 1
URLポリシーを書く
navigateのURLのうち、タスクに要らないものを拒否します。 - 2
ドライバでリクエストを傍受する
クリックやリダイレクトの先も、同じ規則で検査します。
- 3
コンテナにegressポリシーを置く
ドライバから見えない通信を、ネットワーク層で止めます。
- 4
アップロードとダウンロードを閉じ込める
ファイルの出入りを専用ディレクトリに限ります。アップロードを使わない選択もあります。
- 5
影響の大きいメンバーをconfirmで止める
人の承認を挟む呼び出しを決めます。
- 6
ブラウザホストを隔離する
セッションごとに専用のコンテナかVMを使います。
API側で実装する6つの安全対策(分離環境、ドメイン許可リスト、非信頼入力の扱い、スキーム検証、任意メンバーの既定無効、人の確認)は、Browser use toolのセキュリティ対策6原則にまとめています。この記事は、SDKのクラスを使うときに追加で効いてくるURLポリシーと通信遮断に絞ります。
手順1: URLポリシーを関数として渡す
URLポリシーは url_policy(TypeScriptでは urlPolicy)に渡す関数です。navigate が走る前に、SDKがコンテキストと「Claudeが書いたままのURL」を渡して呼び出します。何も返さなければ許可で、拒否するときは ToolError を送出します(TypeScriptではthrow)。Claudeはそのエラーメッセージを読み、navigate は実行されません。
ドキュメントのクイックスタートにある例は、about:blank と2つのサイトおよびそのサブドメインへの http / https だけを通します。公式の例に沿った形は次のとおりです。
import re
from urllib.parse import urlsplit
from anthropic.tools import ToolError
from anthropic.tools.browser import BetaURLContext
ALLOWED_HOSTS = ("example.com", "iana.org")
SCHEME_PREFIX = re.compile(r"[a-z][a-z0-9+.-]*:", re.IGNORECASE)
def is_allowed(url: str) -> bool:
if url.lower() == "about:blank":
return True
with_scheme = url if SCHEME_PREFIX.match(url) else f"https://{url}"
# ブラウザは Web URL の "\" を "/" と読む
try:
parts = urlsplit(with_scheme.replace("\\", "/"))
except ValueError:
return False
host = parts.hostname or ""
listed = any(host == n or host.endswith(f".{n}") for n in ALLOWED_HOSTS)
return parts.scheme in ("http", "https") and listed
def url_policy(context: BetaURLContext, url: str) -> None:
if not is_allowed(url):
raise ToolError(f"blocked: {url} is not on an allowed host")
browser = MyBrowser(backend, url_policy=url_policy)この例は本番向けではない、とドキュメント自身が断っています。読むときの要点は4つあります。
- スキームの補完: Claudeは
example.comのようにスキームを省くことがあります。例は先頭にhttps://を足してからホストを読みます。ドライバのnavigateも同じ判定でスキームを補わないと、ポリシーが検査したURLとブラウザが開くURLがずれます。 - バックスラッシュ: ブラウザはWeb URLの
\を/として読むので、例は検査前に置き換えています。 - スキームの拒否: SDKはスキームを検査しません。
javascript:、view-source:、data:、file:などは、ドライバ側で必ず拒否します。大半のドライバはhttpとhttpsがあれば足ります。 - ポリシーの対象: SDKがポリシーを呼ぶのは
navigateだけです。"back"、"forward"、"reload"では呼ばれません。
url_policy=None(TypeScriptでは urlPolicy: null)を渡すと、SDKはすべてのURLへの navigate を拒否します。設定ファイルから読んだ None が、検査なしの状態を招かないための挙動です。TypeScriptの undefined は、オプションを渡さないのと同じ扱いになります。
本番向けのポリシーには、ホスト名をブラウザと同じ方法で解析すること、ページ内容から来るホスト名やURLを攻撃者が選べる前提で扱うこと、文字列の単純な分割でホスト名を取らないことが求められます。
手順2: ドライバでリクエストを傍受する
URLポリシーが見るのは navigate のURLだけです。クリックしたリンク、送信したフォーム、リダイレクト、許可されたページが読み込む画像やスクリプトは、ポリシーが拒否するホストにも届き得ます。
自動化ライブラリにリクエストフックがあるなら、ドライバでも同じ規則を適用できます。ポリシーと同じ関数を呼べば、規則は1か所で書けます。Playwrightのブラウザコンテキストに登録する例を、公式の例に沿って示します。
class MyBrowser(BetaAbstractBrowserToolset20260801):
...
# context.route("**/*", self._guard) で登録する
def _guard(self, route):
# is_allowed は URL ポリシーの節の関数
if not is_allowed(route.request.url):
return route.abort("blockedbyclient")
route.continue_()このフックには死角があります。WebSocketのハンドシェイク、service workerのリクエスト、リダイレクトの中継ホップは見えません。service workerは無効にし、WebSocketとリダイレクトは、それらを見られるフックで検査します。例のポリシーは ws:// と wss:// を拒否するため、WebSocketのURLを渡すときは先に http:// と https:// に置き換えます。
手順3: コンテナのegressでメタデータアドレスを塞ぐ
傍受でも見えない通信があり、URLポリシーはホスト名がどこに解決されるかも見ません。この2つを補うのが、コンテナのネットワークが強制するegressポリシーです。ドキュメントは3点を挙げています。
- IPv4とIPv6のリンクローカル、プライベートの各アドレス範囲を遮断する。クラウドのメタデータアドレス
169.254.169.254もここに入る - 外向きの接続は、タスクに必要なホストだけに許す。規則がIPアドレスで照合するなら、許可するホスト名はコンテナの起動時に解決しておく
- DNSはコンテナのリゾルバーだけに許す
見落としやすいのはループバックです。KubernetesのNetworkPolicyやクラウドのファイアウォールのように、コンテナの外で効く規則は、コンテナ内のループバックを見ません。そのため、外側の規則だけでは、ページがローカルで待ち受けるものに触れられます。DevToolsのポートも含まれ、そこにはブラウザが開いているタブの一覧と、各タブを操作するためのアドレスが出ます。
つまり、リンクローカルとプライベートの遮断は外側のネットワーク規則でも成立しますが、ループバックの防御はコンテナの内側に置く必要があります。ドキュメントのWarningも同じ区別をしています。
javascript_exec を有効にする場合は、egressルールが外向きの接続を必要なホストに絞っていることが前提です。スクリプトは、ブラウザが到達できるどのアドレスにも、ページの内容を送れるからです。有効化の条件はBrowser use toolを有効化 — javascript_exec/file_uploadの設定とリスクで扱っています。
手順4: アップロードとダウンロードを閉じ込める
file_upload は既定で無効です。file_policy(TypeScriptでは filePolicy)が無ければ、SDKはパスやドキュメントIDを指定するアップロードをすべて拒否します。有効にするなら、そのタスクのファイルだけが入った専用のアップロードディレクトリを1つ指定します。
from anthropic.tools.browser import BetaLocalFilePolicy
# file_upload も実装した MyBrowser
browser = MyBrowser(
backend,
configs={"file_upload": {"enabled": True}},
confirm=make_confirm(), # file_upload には必須
url_policy=url_policy,
file_policy=BetaLocalFilePolicy(
upload_roots=["/task/uploads"],
download_dir="/task/downloads",
expose_download_paths=False,
),
)SDKはシンボリックリンクをたどってアップロードのパスを解決し、アップロードルートの外を指すパスを拒否します。ダウンロードディレクトリがアップロードルートと同一、内側、または外側に含む関係だと、ファイルポリシーは設定エラーにします。ただしこの検査はシンボリックリンクを見逃し得るので、リンクで同一や入れ子の関係を作ってはいけません。
ダウンロード側の設定は、次の4点です。
- ダウンロードディレクトリは自分でモード
0700で作り、noexec,nosuid,nodevでマウントする - シェルやファイルツールなど、Claudeが呼べる他のツールから届かない場所に置く
download_failedの状態変化では、errorを固定の文言にする。例外メッセージにはパスやURLが混ざることがある- ダウンロードしたファイルは、人が決めるまで会話に読み込まず、実行もしない
expose_download_paths がtrueで、かつファイルがダウンロードディレクトリの中にあるときだけ、ダウンロードの path が状態レポートに入ります。既定の False のままなら、Claudeにパスは見えません。
同梱のファイルポリシーが検査するのは、SDKを動かすプロセスのファイルシステム上のパスです。保護できるのは、そのファイルシステムを共有するブラウザだけになります。リモートのブラウザには後述の別の対応が要ります。
手順5: confirmで影響の大きい呼び出しを止める
javascript_exec と file_upload は既定で無効です。どちらかを有効にして confirm を渡さないと、コンストラクタが設定エラーを出します。confirm を渡すと、SDKは実行直前のすべての呼び出しでそれを呼びます。True で実行、False で拒否です。
TypeScriptでは confirm: null が設定エラーを出さず、代わりにSDKが全メンバーの呼び出しを拒否します。
人に尋ねる実装では、メンバー名、ページのURL、呼び出しの入力を見せます。URLも入力もページ由来の文字を含み得るので、表示する前に印字可能なASCII以外をすべてエスケープします。ドキュメントの例は、対象を2メンバーに絞り、承認を「同じページでの完全に同じ入力」に限っています。
GATED = {"javascript_exec", "file_upload"}
def make_confirm():
granted = set()
def confirm(context):
name = context.member
if name not in GATED:
return True
detail = shown(context.input.to_dict()) # ASCII にエスケープした JSON
page = context.tab_url
question = f"Allow {name} on {shown(page) if page else 'a tab with no URL'}?\n{detail}"
if page is None or urlsplit(page).scheme not in ("http", "https"):
return ask_user(question) # Web のオリジンが無ければ毎回尋ねる
key = (name, page, detail)
if key not in granted and ask_user(question):
granted.add(key)
return key in granted
return confirmmake_confirm() の呼び出しごとに、承認が空のコールバックが返ります。ツールセットごとに1回呼び、ユーザーごとに別のツールセットを持たせます。
押さえておく制約が2つあります。
- 承認は、直近の状態レポートが示したページに対するものです。実行前にページが変わっても、SDKはもう一度確認しません。
- 購入、メッセージ送信、規約への同意は、
left_clickやtypeのような通常のメンバーで起きます。confirmは名前だけでは選び分けられないため、人に承認させたいなら、それらのメンバーも対象に入れて尋ねます。
また、navigate の javascript: URLは、javascript_exec が無効でも現在のページでスクリプトを走らせます。confirm から見るとただの navigate なので、例の confirm は承認してしまいます。ドキュメントの例では、confirm より前にURLポリシーが javascript: を拒否します。
手順6: ブラウザホストを隔離する
ブラウザは、セッションごとに専用の最小権限のコンテナかVMで動かします。
- 非rootユーザーで実行し、ブラウザが許すならrootファイルシステムは読み取り専用にする
- 設定したアップロード用・ダウンロード用のディレクトリ以外は、ホストから何もマウントしない
- 環境変数に資格情報を置かず、新しいブラウザプロファイルで始める
- Claudeが呼べる他のツールと、ファイルシステムを共有しない
ブラウザのプロファイルは、Claudeに渡してよいデータと操作しかないアカウントでだけサインインします。javascript_exec はページ自身の権限、つまりCookie、ストレージ、サインイン済みセッションで動くためです。
APIを呼ぶコードはブラウザのコンテナの外で動かします。そのコードがAPIキーと会話を持つからです。ツールランナーでも tool_result でも、ツールセットはそのコードのプロセスで動くので、ブラウザとツールセットはファイルシステムを共有しません。つまりブラウザは、常にリモートとして扱うことになります。ページが返すもの(本文、スクリーンショット、コンソールとネットワークのエントリ、タブのタイトル、ダウンロード名)は、すべて信頼しません。
リモートやホスト型のブラウザでは何が効かなくなるか
別コンテナのブラウザ、DevToolsのURLで接続するブラウザ、ホスト型ブラウザサービスのブラウザは、SDKを動かすプロセスとファイルシステムを共有しません。それでもURLポリシー、リクエスト傍受、confirm は、自分のプロセスで動き続けます。
変わるのは次の点です。
| 項目 | リモートブラウザでの扱い |
|---|---|
| egress | リモートブラウザでの扱いホスト型ではプロバイダーが握る。自分のegressポリシーは効かず、ドライバのリクエストフックが唯一の検査になる |
| 同梱のファイルポリシー | リモートブラウザでの扱いリモートのブラウザは保護されない(ブラウザは自分のファイルシステムを読み書きする) |
file_upload | リモートブラウザでの扱いドライバが、ブラウザの動く場所でパスを検査する独自の BetaFilePolicy を持たない限り、無効のままにする |
| ダウンロード | リモートブラウザでの扱いブラウザのホストが手順4の設定を持たないなら、拒否させる |
| 秘密情報 | リモートブラウザでの扱いプロバイダーのAPIキーと、キーを含み得る接続URLを、ログ、tool_result、エラー文に出さない |
SDKはブラウザがリモートかどうかを検出できません。ホスト型サービスがセッションを録画する場合、その録画はClaudeが見たもの、入力したものすべての複製になり、プロバイダーの保持条件が適用されます。ホスト型を選ぶ前に、そのブラウザのネットワークが何に届くかを調べます。
つまずきやすい4点
- ポリシーを通ったのに内部に届いた: URLポリシーは
navigateの入口の検査にすぎません。リンクのクリックとリダイレクトは、傍受とコンテナのegressで受けます。 javascript:がconfirmを素通りする:confirmからはnavigateに見えます。URLポリシーで先に拒否します。- 外側のNetworkPolicyだけで安心する: ループバック上のDevToolsポートは、外側の規則では塞げません。
- ツールセットを使い回す: 承認の記録はコールバックの中にあります。ユーザーが替わるなら、ツールセットも
make_confirm()も別にします。
6手順のうち、SDKが仕組みを持つのは1、4、5で、2、3、6はドライバとデプロイの側の設計です。SDKクラス自体はベータで、ブラウザ用とコンピューター用の両方がPythonとTypeScriptにあります。セッション内の状態報告の組み立てはbrowser_stateでタブ管理を実装するで、ドライバの基本実装はBrowser Useツールの実装で扱っています。
まとめ
SDKのブラウザツールセットは、URLもスキームもegressも、開発者が足すまで何も見ません。入口はURLポリシーで絞り、道中のリクエストはドライバで傍受し、傍受で見えない通信とメタデータアドレスはコンテナのegressで止めます。ループバックのDevToolsポートだけは、コンテナの内側に防御がないと残ります。
ファイルは専用ディレクトリに閉じ込め、影響の大きい呼び出しは confirm で人に渡し、ブラウザはセッションごとの使い捨て環境に置きます。ホスト型ブラウザを選ぶと、egressと隔離はプロバイダー任せになるため、リクエストフックの精度がそのまま防御の強さになります。