つい先日、著名な LLM API のプロキシサーバである LiteLLM 1 がサイバー攻撃による侵害を受けた。 結果として、攻撃者が不正なコードを挿入したバージョンのパッケージが PyPI 2 にアップロードされた。
現在は既に当該のバージョンは PyPI から削除されている。 しかし、公開されていたタイミングでユーザがインストールした場合には、不正なコードの実行につながる恐れがあった。 不正なコードが実行された場合には、端末がマルウェアに感染してユーザのクレデンシャルを含む情報が外部に送信 (窃取) されるなどの被害が生じた。
この事例から、Python を利用している環境において、PyPI を経由したサプライチェーン攻撃の被害を受けるリスクが現実となった。 したがって、今後は PyPI のユーザ側でもリスクを低減するための行動が重要になってくると考えられる。
今回は、リスクを低減するための対策のひとつを uv 3 を用いて紹介する。 考え方はシンプルで、リリースされたばかりのバージョンのパッケージをインストールしない、というもの。 新しいバージョンのパッケージは、コミュニティによる一定の検証期間を待った上で利用する。 この考え方は Dependency cooldowns という名前で呼ばれることがあるようだ。
使った環境は次のとおり。
$ sw_vers ProductName: macOS ProductVersion: 26.4 BuildVersion: 25E246 $ uname -srm Darwin 25.4.0 arm64 $ uv --version uv 0.11.1 (Homebrew 2026-03-24 aarch64-apple-darwin)
もくじ
下準備
あらかじめパッケージマネージャとして uv をインストールしておく。
$ brew install uv
前提
インストールするパッケージとして FastAPI を例にしてみよう。 FastAPI は比較的リリースサイクルが短いので例にしやすい。
この記事を書いている時点 (2026-03-26) で、FastAPI の直近のバージョンは次の日付でリリースされている。
- 0.135.2: 2026-03-23
- 0.135.1: 2026-03-02
- 0.135.0: 2026-03-01
ここで、仮に 1 週間のクールダウン期間を設ける場合を考える。 つまり、今回であれば 1 週間以上前にリリースされたバージョン 0.135.1 をインストールしたい。
コマンドラインのオプションとして利用する
まずは基本となる uv コマンドのオプションとして使う方法について。
適当な作業用のディレクトリを用意して、仮想環境を初期化する。
$ uv venv --clear Using CPython 3.12.13 interpreter at: /opt/homebrew/opt/python@3.12/bin/python3.12 Creating virtual environment at: .venv Activate with: source .venv/bin/activate
この時点では仮想環境に何のパッケージも入っていない。
$ uv pip list
この環境に 1 週間のクールダウンを設けて FastAPI をインストールしてみよう。
そのためには、オプションとして --exclude-newer="1 week" を指定すれば良い。
$ uv pip install --exclude-newer="1 week" fastapi Resolved 10 packages in 309ms Prepared 1 package in 48ms Installed 10 packages in 10ms + annotated-doc==0.0.4 + annotated-types==0.7.0 + anyio==4.12.1 + fastapi==0.135.1 + idna==3.11 + pydantic==2.12.5 + pydantic-core==2.41.5 + starlette==0.52.1 + typing-extensions==4.15.0 + typing-inspection==0.4.2
すると、ちゃんと FastAPI の 0.135.1 がインストールされていることが分かる。
また、FastAPI の依存パッケージとしてインストールされた anyio のバージョンも見逃せない。 インストールされたバージョンは 4.12.1 になっている。
この記事を書いている時点 (2026-03-26) で、anyio の直近のバージョンは次の日付でリリースされている。
- 4.13.0: 2026-03-24
- 4.12.1: 2026-01-06
- 4.12.0: 2025-11-29
つまり、依存パッケージについてもクールダウンが有効になっている。 このように Dependency cooldowns の考え方は、依存関係のツリー全体に適用される必要がある。
プロジェクトの設定ファイルとして利用する
さて、正直なところ先ほど紹介したコマンドラインのオプションはさほど実用的ではないはず。 というのも、コマンドを実行するときにオプションをつけ忘れる恐れがある。 もし忘れなかったとしても、毎回オプションを意識してつけるのは面倒すぎる。 したがって、設定ファイルにして uv に自動で読んでもらう方がオペレーションとして現実的だろう。
そこで、まずは特定のプロジェクトでポリシーとして Dependency cooldowns を導入する場合を考える。 例えば、チーム開発で複数のメンバーがリポジトリを操作するような場面が想定できる。
このパターンでは、プロジェクトのリポジトリに pyproject.toml か uv.toml を置いて対応する。
設定ファイルを置いておくだけなので、普段のオペレーションには影響が小さい。
あらかじめ uv を使ったワークフローを整備しておけば、メンバーによる結果のバラつきも生じにくい。
uv.toml を使う場合
まずは uv.toml の設定を置く場合から。
これは単純に exclude-newer の 1 行が入った uv.toml を置くだけで良い。
$ cat << EOF > uv.toml exclude-newer = "1 week" EOF
一旦、先ほど作った仮想環境をリセットしておこう。
$ uv venv --clear Using CPython 3.12.13 interpreter at: /opt/homebrew/opt/python@3.12/bin/python3.12 Creating virtual environment at: .venv Activate with: source .venv/bin/activate
そして、改めてオプションを特につけずに FastAPI をインストールする。
$ uv pip install fastapi Resolved 10 packages in 6ms Installed 10 packages in 6ms + annotated-doc==0.0.4 + annotated-types==0.7.0 + anyio==4.12.1 + fastapi==0.135.1 + idna==3.11 + pydantic==2.12.5 + pydantic-core==2.41.5 + starlette==0.52.1 + typing-extensions==4.15.0 + typing-inspection==0.4.2
すると、インストールされたバージョンから設定が有効に働いていることが確認できる。
$ uv pip list | grep fastapi fastapi 0.135.1
確認が終わったら次の実験に向けて設定ファイルを削除しておく。
$ rm uv.toml
pyproject.toml を使う場合
最近の Python を使ったプロジェクトであれば pyproject.toml を用意している場合が多いはず。
その場合は、次のようにして既存のファイルの中に [tool.uv] セクションを追加しても良い。
[project] name = "example-project" version = "0.0.1" requires-python = ">=3.12" [tool.uv] exclude-newer = "1 week"
先ほど作った仮想環境をリセットしておこう。
$ uv venv --clear Using CPython 3.12.13 interpreter at: /opt/homebrew/opt/python@3.12/bin/python3.12 Creating virtual environment at: .venv Activate with: source .venv/bin/activate
そして、改めて特にオプションを指定せずにプロジェクトの依存パッケージとして FastAPI を追加する。
$ uv add fastapi Resolved 11 packages in 10ms Installed 10 packages in 7ms + annotated-doc==0.0.4 + annotated-types==0.7.0 + anyio==4.12.1 + fastapi==0.135.1 + idna==3.11 + pydantic==2.12.5 + pydantic-core==2.41.5 + starlette==0.52.1 + typing-extensions==4.15.0 + typing-inspection==0.4.2
すると、インストールされたバージョンから設定が有効に働いていることが確認できる。
この設定は、依存パッケージのバージョンロックを更新する際にも、もちろん反映される。
$ uv lock --upgrade --dry-run Resolved 11 packages in 126ms Lockfile changes detected
通常のポリシーとは外れた条件を特定のパッケージに入れたいときは exclude-newer-package を使う。
例えば、緊急での脆弱性対応リリースなどが想定される。
$ echo 'exclude-newer-package = { fastapi = "0 day" }' >> pyproject.toml $ tail -n 3 pyproject.toml [tool.uv] exclude-newer = "1 week" exclude-newer-package = { fastapi = "0 day" }
こうすればピンポイントでバージョンを上げられる。
$ uv lock --upgrade --dry-run Resolved 11 packages in 131ms Update fastapi v0.135.1 -> v0.135.2
確認が終わったら作成したファイルを削除しておく。
$ rm pyproject.toml $ rm uv.lock
ユーザの設定ファイルとして利用する
先ほどのやり方は、プロジェクトに紐づいていた。 一方で、普段の何気ない操作でも常に有効にしたいニーズはあるはず。 そんなときは、ユーザの設定ファイルにしておくと良い。
やることは単純で ~/.config/uv/uv.toml に設定を置くだけ。
$ mkdir -p ~/.config/uv $ cat << EOF >> ~/.config/uv/uv.toml exclude-newer = "1 week" EOF
仮想環境をリセットしておく。
$ uv venv --clear Using CPython 3.12.13 interpreter at: /opt/homebrew/opt/python@3.12/bin/python3.12 Creating virtual environment at: .venv Activate with: source .venv/bin/activate
この状態で仮想環境に FastAPI をインストールする。
$ uv pip install fastapi Resolved 10 packages in 5ms Installed 10 packages in 6ms + annotated-doc==0.0.4 + annotated-types==0.7.0 + anyio==4.12.1 + fastapi==0.135.1 + idna==3.11 + pydantic==2.12.5 + pydantic-core==2.41.5 + starlette==0.52.1 + typing-extensions==4.15.0 + typing-inspection==0.4.2
ちゃんとバージョン 0.135.1 がインストールされている。
まとめ
今回は PyPI を経由したサプライチェーン攻撃のリスクを低減する手法のひとつとして Dependency cooldowns を扱った。 この手法は導入に必要なコストが小さく、リスクを低減する効果もそれなりに大きい。 そうした背景もあって、最近のパッケージマネージャでは言語を問わず導入が進んでいる。 万全ではないことを理解した上で、ワークフローに組み込んで利用する余地があるだろう。
