CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: uv でパッケージのインストールにクールダウン期間を設ける

つい先日、著名な LLM API のプロキシサーバである LiteLLM 1 がサイバー攻撃による侵害を受けた。 結果として、攻撃者が不正なコードを挿入したバージョンのパッケージが PyPI 2 にアップロードされた。

github.com

現在は既に当該のバージョンは PyPI から削除されている。 しかし、公開されていたタイミングでユーザがインストールした場合には、不正なコードの実行につながる恐れがあった。 不正なコードが実行された場合には、端末がマルウェアに感染してユーザのクレデンシャルを含む情報が外部に送信 (窃取) されるなどの被害が生じた。

この事例から、Python を利用している環境において、PyPI を経由したサプライチェーン攻撃の被害を受けるリスクが現実となった。 したがって、今後は PyPI のユーザ側でもリスクを低減するための行動が重要になってくると考えられる。

今回は、リスクを低減するための対策のひとつを uv 3 を用いて紹介する。 考え方はシンプルで、リリースされたばかりのバージョンのパッケージをインストールしない、というもの。 新しいバージョンのパッケージは、コミュニティによる一定の検証期間を待った上で利用する。 この考え方は Dependency cooldowns という名前で呼ばれることがあるようだ。

blog.yossarian.net

使った環境は次のとおり。

$ 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 は比較的リリースサイクルが短いので例にしやすい。

pypi.org

この記事を書いている時点 (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 になっている。

pypi.org

この記事を書いている時点 (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.tomluv.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 を扱った。 この手法は導入に必要なコストが小さく、リスクを低減する効果もそれなりに大きい。 そうした背景もあって、最近のパッケージマネージャでは言語を問わず導入が進んでいる。 万全ではないことを理解した上で、ワークフローに組み込んで利用する余地があるだろう。

参考

docs.astral.sh