Claude Media
Claude Codeの拒否ルールをeval等ですり抜けられる問題と対策

Claude Codeの拒否ルールをeval等ですり抜けられる問題と対策

permissions.denyのRead/Editルールはevalやenv -Cで包んだBashコマンドを検知できません。v2.1.268の修正とv2.1.273のリバートの経緯と、サンドボックスやフックを重ねる対策を解説します。

Claude Codeのpermissions.denyはコマンド文字列だけを見て判定する

Claude Codeのpermissions.denyルールは、Claudeが書いたコマンドの文字列を解析して一致を判定します。Bashルールは、Claudeが普段出力する呼び出しを止める仕組みであり、プログラムそのものを囲うセキュリティ境界ではありません。

この設計のため、eval '...'やenv -C . sh -c '...'のようにシェルへ文字列をそのまま渡す形で包むと、パーサーが中身を解析できなくなります。changelogには、env -C・evalなど権限チェッカーが解析できないコマンドが同じ行にあると、ReadまたはEditの拒否ルールが適用されないことがある、という不具合が記録されています。

Read(./secrets/**)を設定したときに何が止まり、何が止まらないかを分けると次のようになります。

くらべる

Read(./secrets/**)を設定したときの行き先

止まる

拒否ルールが効く形

  • Readツールによる読み取り(GrepやGlobにもベストエフォートで適用)
  • cat・head・tail・sed・teeなど、Claude Codeがファイルを扱うと認識するBashコマンド
  • < fileや> fileのようなリダイレクトの対象
止まらない

効かない、またはプロンプトに戻る形

  • eval '...'やenv -Cで包んだ行(v2.1.273以降は確認プロンプト)
  • ファイル名を書かずに読むコマンド。そのファイルがあるディレクトリでのgrep -r pattern .など
  • ファイルを自分で開くPythonやNodeのスクリプトのような子プロセス

拒否の範囲を広げては戻された5つのバージョン

Bashの行にどこまで拒否ルールを適用するかは、v2.1.257からの変更履歴で広げては戻すことが繰り返されています。evalの話は、この繰り返しの2回目にあたります。

あゆみ

Read/Edit拒否ルールの適用範囲をめぐる変更

  1. v2.1.257リダイレクトと読み取りコマンドに適用

    BashのRead()・Edit()拒否ルールが、< fileのリダイレクトやtac・egrepなどの読み取りコマンドにも効くようになりました。

  2. v2.1.259 → v2.1.260引数への適用が1バージョンで戻る

    v2.1.259でオプションの値(--ignore-revs-file=.env)やgit diffのオペランドにも広がりましたが、v2.1.260でリバートされました。Read(./**/build/**)のもとでnpm run buildが全モードで拒否され、cd … && grepがautoモードでもプロンプトを出したためです。

  3. v2.1.268解析できない行にも適用

    env -C・evalなど権限チェッカーが解析できないコマンドが同じ行にあっても、ReadとEditの拒否ルールを適用する修正が入りました。

  4. v2.1.273v2.1.268の修正をリバート

    time -p make buildのような正当なコマンドまで拒否されたため、元の「プロンプトで確認する」挙動に戻りました。

文字列の照合を広げるたびに、無関係なコマンドの誤拒否が出て巻き戻されています。照合を狭めれば迂回が残り、広げれば誤拒否が出る。この綱引きが続いているため、拒否ルールの改善だけで迂回を消し切る方向は取りにくい構造と読めます。v2.1.274からv2.1.289までのchangelogには、解析できない行へ拒否ルールを再適用する変更は載っていません。

モード別に見る、evalで包んだ行の行き先

Manualモード(設定値default)では、v2.1.273の挙動どおり、evalやenv -Cで包んだ行にも実行前の確認プロンプトが出ます。ユーザーがそこで気づいて拒否すれば実害はありません。

autoモードでは、行が次の順に判定されます。

手順

autoモードの判定順序(先に一致した段で決着)

  1. 1

    allow・ask・denyルールに一致するか

    一致すればその場で決着します。一致しなければ、次の段へ進みます。

  2. 2

    読み取り専用か、作業ディレクトリ内の編集か

    当てはまれば自動承認されます。

  3. 3

    それ以外は分類器へ

    分類器がブロックすると、Claudeに理由が返ります。

evalの行が拒否ルールに一致しなくても、そこで通ったことにはならず、autoモードなら次の段の判定へ進みます。autoモード側に独自ルールを足す方法はClaude CodeのautoModeに独自ルールを追加する書き方にあります。

一方、分類器はautoモードの仕組みで、bypassPermissionsモード(--dangerously-skip-permissions)にはありません。拒否ルールはbypassPermissionsでも効きますが、解析できない行についてv2.1.273が述べる確認プロンプトは、プロンプトを省くモードでは前提にできません。評価の網が拒否ルールの文字列照合だけになるため、次のような行は実行前に気づく機会がなくなる可能性があります。

# permissions.deny に "Read(./secrets/**)" を設定していても
eval 'cat ./secrets/api-key.txt'
env -C . sh -c 'cat ./secrets/api-key.txt'

同じ内容を直接cat ./secrets/api-key.txtと書けば拒否ルールに一致して止まりますが、evalやenv -Cで一段包むだけでパーサーの解析対象から外れます。

同じ設計限界は他のBashルールにも及ぶ

この問題はevalやenv -Cだけの欠陥というより、「コマンド文字列を静的解析して一致を取る」という拒否ルール全体の設計に由来します。denyやaskに置いたBashルールが止める形と止めない形は、次のとおりです。

ルール止まる止まらない
Bash(curl *)止まるcurl https://example.com止まらない/usr/bin/curl https://example.com、sh -c 'curl https://example.com'
Bash(rm *)止まるrm -rf build/止まらない/bin/rm -rf build/、bash -c 'rm -rf build/'
Bash(git push *)止まるgit push origin main止まらないgit -C . push origin main、git 'push' origin main

timeout・time・nice・nohup・stdbufのほか、command・builtin・zshのnoglob、フラグなしのxargsは、ルール照合の前に取り除かれる組み込みのラッパーです。Bash(npm test *)がtimeout 30 npm testにも一致するのはこのためです。sh -c・bash -c・evalは取り除かれる一覧に入っていないので、中身を渡すだけのラッパーとして素通りします。止まらない形が実際に実行されるかどうかは、権限モードと他のルールが決めます。コマンド文字列に依存しない境界が必要なら、次節のサンドボックスがその役割を担います。

設定例 — Read/Editの拒否ルール、サンドボックス、PreToolUseフック

保護したいファイルがある場合、settings.jsonにはRead/Editの拒否ルールをパスで指定します。

{
  "permissions": {
    "deny": ["Read(./secrets/**)", "Edit(./secrets/**)", "Read(./.env)"]
  }
}

このルールだけでは、前述のeval 'cat ./secrets/api-key.txt'のような行が確認プロンプト頼みになります。そこでサンドボックスを加えますが、sandbox.enabled: trueの既定設定は書き込みを作業ディレクトリ配下に絞るだけで、読み取りは一部の拒否ディレクトリを除きコンピューター全体に開いています。./secrets/は作業ディレクトリの内側にあるので、既定のサンドボックスだけではこの読み取りを止められません。読み取りも塞ぐにはsandbox.filesystem.denyReadにパスを追加します。

{
  "sandbox": {
    "enabled": true,
    "filesystem": {
      "denyRead": ["./secrets"]
    }
  }
}

サンドボックスの境界はコマンド文字列ではなく、実行中のプロセスにOSが強制する仕組みです。evalで中身を隠されても、denyReadで指定したパスへのアクセスはブロックされます。ただしdenyReadが止めるのはサンドボックス内のBashコマンドで、Readツールは止まりません。Readツールは上のRead(./secrets/**)が受け持つので、2つの設定は両方そろえて意味を持ちます。

設定の詳細とallowReadでの例外の開け方はsandbox.filesystem.denyReadで認証情報ファイルを読ませないにまとめています。

PreToolUseフックを併用すると、権限ルール評価とは別のレイヤーでコマンド文字列そのものを検査できます。フックはexit code 2で終了するとツール呼び出しを止め、標準エラー出力に書いた理由がClaudeに返ります。まずsettings.jsonでBashツールに登録します。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/deny-eval-secrets.sh"
          }
        ]
      }
    ]
  }
}

検査するスクリプトは次のとおりです。

#!/bin/bash
# .claude/hooks/deny-eval-secrets.sh (PreToolUse, matcher: Bash)
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq '(eval|env -C).*secrets'; then
  echo "secretsディレクトリへのeval/env -C経由のアクセスを検知したため拒否します" >&2
  exit 2
fi

フックはClaude Codeと独立したシェルスクリプトなので、Claude Codeが渡すJSONの形を手で作れば単体で試せます。5種類の入力を流した結果は次のとおりです。

jq -n --arg c "eval 'cat ./secrets/api-key.txt'" '{tool_input:{command:$c}}' | ./deny-eval-secrets.sh; echo "exit=$?"
# secretsディレクトリへのeval/env -C経由のアクセスを検知したため拒否します
# exit=2
 
# 入力ごとの結果
#   env -C . sh -c 'cat ./secrets/api-key.txt'  → 同じメッセージで exit=2
#   sh -c 'cat ./secrets/api-key.txt'           → exit=0(素通り)
#   eval 'cat ./sec''rets/api-key.txt'          → exit=0(素通り)
#   cat ./secrets/api-key.txt                   → exit=0(フックは何もせず、拒否ルール側で止まる)

evalとenv -Cは止まりますが、sh -cで包んだ行や、'sec''rets'のように文字列を分割した行は通ります。簡易フックは、狙った迂回の形だけを止める道具です。キーワードを増やしても書き方の自由度には追いつけないので、境界としてはサンドボックスのdenyReadが土台になります。

層ごとの役割 — permissions.denyだけに頼らない

permissions.denyは、Claudeが書いたコマンドをどう扱うかを決める一次の防波堤です。任意のシェル構文を完全に解析するようには作られていません。守る対象ごとに受け持つ層が違うので、次のように重ねます。

  1. Readツールの経路はRead拒否ルール、Bashの子プロセスはsandbox.filesystem.denyRead: 前節のとおり、片方だけでは反対側の経路が開きます。
  2. permissions.blockReadsOutsideWorkingDirectoriesは作業ディレクトリの外を塞ぐ設定: ファイルツールによる作業ディレクトリ外の読み取りを、bypassPermissionsを含む全モードで拒否します。サンドボックスを有効にしていれば、サンドボックス内のコマンドもホームディレクトリなどの読み取りを失います。シェルコマンド自体を拒否するわけではありません。この設定を有効にしている場合、サブシェルや複数回のcdを含む行のようにパーサーが追えないコマンドは、autoモードとbypassPermissionsでも確認プロンプトになります。サンドボックスが読み取りの制限を強制しているコマンドでは、このプロンプトは出ません。./secrets/は作業ディレクトリの内側にあるので、この設定の対象には入りません。
  3. PreToolUseフックでコマンド文字列を自前で検査する: フックはルール評価と独立に動き、exit code 2の終了は許可ルールがあっても呼び出しを止めます。上の簡易フックのように、通したくない形を具体的に列挙します。
  4. --dangerously-skip-permissionsは隔離環境に限る: このモードはコンテナやVMなど、ホストに被害を出せない環境向けです。ホスト上で使うと確認プロンプトという網が消え、evalによる素通りを止めるのは、サンドボックスとフックのように権限ルールの外側にある仕組みだけになります。

組織で拒否ルールを配布するなら、ルール側を固める方法があります。/permissionsコマンドで権限ルールとauto mode拒否を管理するやallowManagedPermissionRulesOnlyで権限ルールを管理設定だけに縛るが入口です。ただしサンドボックス側の設定も同じ場所に置かないと、この記事の迂回は残ります。

まとめ

permissions.denyのRead/Editルールは、evalやenv -Cのように中身をシェルへ渡す行までは追えません。v2.1.268の修正はv2.1.273でリバートされ、解析できない行は確認プロンプトに戻っています。守りたいパスがあるなら、Readツールは拒否ルール、Bashの子プロセスはdenyReadと、経路ごとに受け持つ層を分け、必要に応じてフックを足します。

この記事を共有:XはてブLinkedIn