0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIが提案した pip install、その名前、本当に存在しますか? — パッケージ幻覚とslopsquattingから依存関係を守る実践ガイド

0
Posted at

はじめに — AIが出した「便利そうなライブラリ」、そのまま入れてません?

正直に言いましょう。

AIにコードを書いてもらって、その中に pip install super-fast-json とか npm install react-use-toast-pro みたいな行があったとき、名前を疑わずにそのままコピペして実行したこと、ありませんか。

僕はあります。だって、AIって自信満々で言ってくるんですよね。「このライブラリを使えば一発です」みたいな顔で。

でも、ちょっと考えてみましょう。そのパッケージ、本当にこの世に存在しますか。

実は、AIコーディングが当たり前になった今、いちばん静かに、いちばん深いところに刺さろうとしている攻撃があります。名前を slopsquatting(スロップスクワッティング) と言います。ざっくり言うと、「AIがよく口にする、存在しないライブラリ名」を、攻撃者が先回りして本物のレジストリに登録しておく、という手口です。

あなたがAIの言うとおりにコピペした瞬間、その「存在しないはずの名前」が、いつのまにか「攻撃者が用意した本物のパッケージ」に変わっている……。SFみたいですけど、これ、2026年の今、現実に起きている話なんです。

この記事は、脅すための記事じゃありません。AIに安心して開発を任せるための「柵(さく)」を、今日から自分の手で作るための、実践ガイドです。コードもプロンプトもそのまま持って帰れるように、厚めに置いていきますね。


そもそも「幻覚」と「slopsquatting」ってなに?

無知の無知でも大丈夫なように、まず言葉から揃えましょう。知らない用語で置いていかれるの、いちばんもったいないので。

  • パッケージ / ライブラリ … 他の人が作ってくれた便利な部品。自分でゼロから書かずに import して使えます。
  • レジストリ … その部品が並んでいる公式の棚。Pythonなら PyPI、JavaScript/Node.jsなら npm が有名です。pip installnpm install は、この棚から部品を取ってくる動作ですね。
  • ハルシネーション(幻覚) … AIが、事実じゃないことを、さも事実のように自信満々に言ってしまう現象。ここでは「実在しないライブラリ名を、実在するかのように勧めてくる」ことを指します。
  • typosquatting(タイポスクワッティング) … 昔からある手口で、requests に対して reqeusts みたいな「打ち間違いを狙った偽物」をレジストリに置いておくやつ。人間のタイプミスを釣る罠です。

そして本題の slopsquatting は、このtyposquattingの進化系です。何が違うかというと、狙う相手が人間のミスじゃなくて、AIの幻覚のほうだという点なんですよ。

たとえるなら、こういうことです。

道に迷ったあなたが、すごく物知りな案内役(AI)に「近くにおいしいカレー屋さんある?」と聞いたら、「ありますよ、"スーパーカレー横丁"です」と即答された。でも実は、そんな店は存在しなかった。ところが、その案内役が毎回同じように "スーパーカレー横丁" と言うことに気づいた誰かが、先回りしてその名前で偽の店を出した……。あなたは案内どおりに入って、まんまと騙される。

これがslopsquattingの構造です。AIが「毎回同じ嘘の名前」を言う、という予測可能性が、そのまま攻撃の入口になってしまう。ここがミソなんです。


数字で見ると、これは「まれなバグ」じゃなくて「構造」だった

「でも、そんなの滅多に起きひんのちゃう?」と思いますよね。僕も最初そう思ってました。

ところが、2025年のセキュリティ国際会議 USENIX Security で発表された大規模な研究("We Have a Package for You!"、arXiv:2406.10279)を読むと、けっこう肝が冷えます。この研究は、商用・オープンソース合わせて 16個のコード生成モデル に、57万6千件 ものコードを書かせて、勧めてきたパッケージが本当に実在するかを一個ずつ照合したものです。

主な結果を、事実として並べますね。

  • 勧められたパッケージのうち、約19.7%が、そもそもレジストリに存在しなかった
  • 商用モデルの平均は 約5.2%、オープンソースモデルの平均は 約21.7%(一部は33%超)
  • いちばん優秀だったモデルでも 約3.59% は幻覚を出した
  • 存在しないユニークな名前は、20万5千個以上 見つかった
  • しかも、同じ質問を10回くり返すと、約43%が毎回まったく同じ幻覚名を出した

最後のやつ、地味にいちばん怖くないですか。ここが本質だと思っていて、AIは、でたらめを、再現性をもって言うんです。ランダムに間違えてくれるなら攻撃者も狙いを定めにくいけど、毎回同じ名前を言うなら、攻撃者は「AIがよく言う嘘の名前トップ100」を作って、先に登録して待っていればいい、ということになる。

実際、幻覚の名前がそのまま実害になった例もあります。ある幻覚パッケージ名は、3ヶ月で3万回以上もダウンロードされてしまった、という報告があります。だれかが悪意を持ってそこに中身を入れていたら……と考えると、これはもう「まれな事故」じゃなくて、構造的な現象として向き合うべきテーマだと思うんです。

念のため補足すると、これは「AIが悪い」「特定のモデルがダメ」という話じゃないです。優秀な商用モデルでも数%は出る。つまり誰の環境でも起こりうる。だからこそ、モデルを責めるんじゃなくて、仕組みで受け止める。そういう話なんですね。


なぜAIは、存在しない名前を「自信満々」に勧めるのか

ここ、理解しておくと防御の勘所がわかります。

LLM(AI)は、乱暴に言うと「次に来そうな文字列を、確率的にそれっぽく生成する」機械です。pip install の続きとして、学習データにあった無数のパッケージ名の「かけら」を組み合わせて、"いかにも存在しそうな名前" を作ります。

問題は、AIが「その名前が本当にレジストリにあるか」を確認していないこと。存在チェックの機能が内蔵されているわけじゃなくて、あくまで「ありそう」を出しているだけ。なのに、口調だけは自信満々。この「自信の顔」と「未確認の中身」のギャップこそが、罠の正体です。

幻覚の出方には、だいたい3パターンあります。研究で分類されていたものを、ダミー名でまとめておきますね。

パターン 何が起きるか 例(すべてダミー) なぜ気づきにくいか
混同(conflation・約38%) 実在する複数の名前が混ざる python-dateutils(実在は python-dateutil 本物にそっくりで見分けにくい
タイポ系(約13%) 1〜2文字だけズレる reqeusts(実在は requests 自分の打ち間違いと勘違いする
完全な創作(約51%) どこにも無い名前を作る super-fast-jsonx 「便利そう」で信じてしまう

半分以上が「完全な創作」なんですよね。つまり、実在チェックさえすれば、かなりの割合をその場で弾ける。逆に言うと、チェックしないと半分は素通りする、ということでもあります。


攻撃が「刺さる」導線は、だいたいこの3つ

怖い仕組みがわかったところで、じゃあ実際どこで刺さるのか。開発フローの中で危ないポイントを、正直に洗い出しておきましょう。敵の顔を見ておくと、守る場所が決まります。

  1. 人間のコピペ実行 … AIの回答にあった pip install ○○ を、名前を確認せずそのままターミナルへ。いちばん古典的で、いちばん多い。
  2. AIエージェントの自動install … 最近はAIが自分でコマンドを実行する。人間が見ていないところで install まで走ると、幻覚名がそのまま入る。
  3. 依存ファイルのAI自動生成 … 「requirements.txt 作っといて」と頼むと、AIが実在しない依存を混ぜて書くことがある。レビューを通さず pip install -r すると素通り。

共通しているのは、実在と素性を確かめる一手が、どこにも挟まっていないことです。逆に言えば、この一手をどこかに1個入れるだけで、導線はかなり細くなります。じゃあ、その一手をどう設計するか。ここからが本題です。


本題 — 人間が設計する「依存の信頼ゲート」

いちばん大事な原則を、先に置きます。

AIは「名前を提案する係」。実在と素性を確かめるのは、人間とCIの仕事。

この線引きが、今日つくる設計の背骨です。AIに「名前を出すな」と言うのは無理があるし、もったいない。AIはむしろどんどん候補を出していい。ただ、その候補を"信じて実行に移す"ところだけは、人間と自動チェックが握る。ここを混ぜないことが肝心なんです。

「AIがすごい」で終わらせず、どこを任せて、どこを握るか。具体的にコードで固めていきましょう。

防御コード1:PyPIで「実在+素性」を確かめる(Python)

まずは基本の一手。パッケージ名を渡すと、PyPIに実在するか、そして「生まれたてすぎないか」まで見て判定します。PyPIは https://pypi.org/pypi/{名前}/json を叩くと、存在すれば情報が、無ければ404が返ってくるので、これを使います。

import sys
import json
import urllib.request
import urllib.error
from datetime import datetime, timezone


def inspect_pypi_package(name: str) -> dict:
    """PyPIに実在するか+素性(公開日/バージョン数)を返す。"""
    url = f"https://pypi.org/pypi/{name}/json"
    try:
        with urllib.request.urlopen(url, timeout=10) as res:
            data = json.load(res)
    except urllib.error.HTTPError as e:
        if e.code == 404:
            return {"name": name, "exists": False,
                    "reason": "レジストリに存在しない(幻覚の可能性)"}
        raise

    releases = data.get("releases", {})
    upload_times = [
        f["upload_time_iso_8601"]
        for files in releases.values() for f in files
        if f.get("upload_time_iso_8601")
    ]
    first_seen = min(upload_times) if upload_times else None
    age_days = None
    if first_seen:
        created = datetime.fromisoformat(first_seen.replace("Z", "+00:00"))
        age_days = (datetime.now(timezone.utc) - created).days

    return {
        "name": name,
        "exists": True,
        "version_count": len(releases),
        "first_seen": first_seen,
        "age_days": age_days,
    }


def judge(info: dict) -> str:
    if not info["exists"]:
        return "STOP: " + info["reason"]
    # 実在しても「生まれたて+バージョン1個」は要人間確認
    if (info.get("age_days") or 0) < 30 and info.get("version_count", 0) <= 1:
        return "REVIEW: 実在するが新しすぎる。素性を人間が確認"
    return "OK: 実在・履歴あり(最終判断は人間)"


if __name__ == "__main__":
    bad = 0
    for pkg in sys.argv[1:]:
        verdict = judge(inspect_pypi_package(pkg))
        print(f"{pkg} -> {verdict}")
        if verdict.startswith("STOP"):
            bad += 1
    sys.exit(1 if bad else 0)

ポイントは、「実在するか」だけじゃなくて「新しすぎないか」まで見ているところです。slopsquattingの偽物は、たいてい「つい最近登録されたばかり」で「バージョンが1個だけ」。だから、実在してても生まれたてなら、いったん人間の目を通す。ここが効きます。STOP のときは終了コード1を返すので、あとでCIにそのまま使えます。

防御コード2:npmで素性を見て、許可リストと照合する(Node.js)

Node.js側も同じ発想でいきましょう。npmは https://registry.npmjs.org/{名前} で情報が取れます。ここでは allowlist(チームで承認済みの定番リスト) と照らして、知らない依存だけを人間レビューに回す形にします。

// verify-npm-deps.mjs — AIが追加した依存を実在性・素性・許可リストで判定
import { readFile } from "node:fs/promises";

// チームで「これは使ってOK」と合意した定番だけを入れる
const ALLOWLIST = new Set(["express", "zod", "react", "vitest"]);

async function inspectNpmPackage(name) {
  const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}`);
  if (res.status === 404) {
    return { name, exists: false, reason: "レジストリに存在しない(幻覚の可能性)" };
  }
  const data = await res.json();
  const created = data.time?.created ? new Date(data.time.created) : null;
  const ageDays = created
    ? Math.floor((Date.now() - created.getTime()) / 86_400_000)
    : null;
  return {
    name,
    exists: true,
    ageDays,
    maintainers: (data.maintainers || []).length,
    latest: data["dist-tags"]?.latest,
  };
}

function judge(name, info) {
  if (ALLOWLIST.has(name)) return "OK: 許可リスト済み";
  if (!info.exists) return `STOP: ${info.reason}`;
  if ((info.ageDays ?? 0) < 30) return "REVIEW: 公開30日未満。素性を人間が確認";
  return "REVIEW: 実在するが未承認。allowlist入りを人間が判断";
}

const pkg = JSON.parse(await readFile("package.json", "utf8"));
const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });

let bad = 0;
for (const name of deps) {
  const info = await inspectNpmPackage(name);
  const verdict = judge(name, info);
  console.log(`${name} -> ${verdict}`);
  if (verdict.startsWith("STOP")) bad += 1;
}
process.exit(bad ? 1 : 0);

allowlist方式のいいところは、「知ってるものは通す、知らないものは止める」というデフォルト拒否の発想になっていること。新しい依存を足すたびに人間が一瞬立ち止まる。この「一瞬」が、明日の自分を守ります。

防御コード3:CIで「新しく足された依存だけ」を自動で門番する

個人の心がけだけだと、忙しい日にすり抜けます。だから、仕組みに落とします。プルリクエストで新しく追加された依存だけを取り出して、コード1で検証し、STOP が出たらマージを止めるGitHub Actionsです。

# .github/workflows/dep-guard.yml
name: dependency-guard
on: [pull_request]

jobs:
  guard:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0            # 差分を見るため履歴も取得
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: 追加された依存だけを検証する
        run: |
          # このPRで requirements.txt に「追加」された行だけを抜き出す
          git diff "origin/${{ github.base_ref }}...HEAD" -- requirements.txt \
            | grep -E '^\+[^+]' | sed 's/^\+//' \
            | grep -vE '^\s*#' | cut -d= -f1 | tr -d ' ' \
            | while read -r pkg; do
                [ -z "$pkg" ] && continue
                echo "checking: $pkg"
                python verify_pypi.py "$pkg" || exit 1
              done

verify_pypi.py は、さっきのコード1をそのまま置くだけ。差分の中の"新規追加ぶん"だけを見るのがコツです。既存の依存を毎回全部チェックすると重いし、うるさい。「今、増えたものだけ」に集中すると、実運用でちゃんと回ります。最初は落とさず警告だけにして、慣れてきたら exit 1 で止める、くらいの温度感でも十分だと思います。

仕上げ:ピン留めで「名前のすり替え」を封じる

最後に、そもそも「同じ名前でも中身がすり替わる」事故を防ぐ、ハッシュ固定です。これはslopsquattingだけでなく、依存の改ざん全般に効きます。

# pip: ハッシュ付きで固定し、中身のすり替えを弾く
pip install pip-tools
pip-compile --generate-hashes requirements.in    # requirements.txt にハッシュを埋める
pip install --require-hashes -r requirements.txt  # ハッシュ不一致は問答無用で失敗

# npm: lockfile どおりに入れ、インストール時スクリプトを止める
npm ci --ignore-scripts

--ignore-scripts は地味に大事です。悪意あるパッケージは postinstall で外部に通信したりするので、まずスクリプトを走らせずに入れて、中身を確認してから、というワンクッションが効きます。


そのまま使えるプロンプト例3本

コードで門番を作りつつ、AIとのやり取り自体も安全側に寄せておきましょう。ここはプロンプトの出番です。

プロンプト1:生成の段階で「検証テーブル」を義務づける

あなたはシニアエンジニアです。次の要件のコードを書いてください。

依存に関する制約(必ず守る):
- 外部ライブラリを import / install する場合は、実在が確実な定番のみを使う
- 使った各パッケージについて、本文の末尾に「検証テーブル」を必ず付ける:
  | パッケージ名 | 公式レジストリ(PyPI/npm)のURL | 用途 | 標準ライブラリで代替可能か |
- 少しでも自信がないパッケージは本体では使わず、「未確認候補」として別枠に分け、理由を書く
- 存在をでっち上げないこと。知らないものは正直に「知らない」と書く

要件:
<ここに作りたい機能を書く>

AIに「URLを併記して」と言うと、自分が出した名前と一度向き合うステップが挟まります。完全ではないけど、丸腰でコピペするよりずっとマシになります。

プロンプト2:差分から「新規依存」をトリアージさせる

次のPR差分から「新しく追加された依存パッケージ」を全て抽出し、1つずつトリアージしてください。

出力(Markdownの表):
| パッケージ | 追加理由(差分から推測) | 幻覚/typosquatの疑い | 代替(標準ライブラリ/既存依存) | 人間確認が必要か |

判定ルール:
- 既存の定番と名前が「似すぎている」ものは typosquat / conflation を疑う
- 差分の中で実際には使われていない依存は「不要の可能性」に分類する
- 判断できないものは必ず「要人間確認」に倒す(勝手にOKにしない)

差分:
<ここに git diff を貼る>

プロンプト3:依存リスト全体の監査レポート

次の依存リストを監査し、リスクの高い順に並べたレポートを作ってください。

各パッケージを次の観点で表にする:
| パッケージ | 公開からの経過 | 週間DL規模の目安 | メンテナ数の目安 | 既知の懸念 | リスク(高/中/低) |

ルール:
- 数値が確認できない項目は正直に「不明」と書く(不明であること自体がリスク信号)
- 「新しい × DLが少ない × メンテナ1人」が重なるものは最優先で人間レビューへ
- 各行に「次の一手(承認 / 保留 / 置換 / 削除)」を1つ添える

依存リスト:
<requirements.txt か package.json の中身を貼る>

3本に共通する思想は、AIに判断させず、AIには"材料を並べさせる"だけにするという点です。最終判断は人間が握る。AIは表を埋める係、人間は意味を決める係。役割を混ぜないのがコツです。


判断を助ける3つの表

信頼シグナル判定表 — 「怪しさ」を勘じゃなく信号で見る

シグナル 安心寄り 要注意寄り 確かめ方
実在 レジストリに有り 404が返る PyPI / npm の JSON API
年齢 数ヶ月〜数年 数日〜数週間 first upload / time.created
DL規模 多い ごくわずか pypistats / npm の週間DL
メンテナ 実績ある複数・組織 匿名1人・新規 maintainers 情報
使用実態 コードで実際に使う importされていない 差分・静的解析

1個だけで白黒つけないのがコツです。「新しい」だけなら良い新作かもしれない。でも「新しい × DL少 × メンテナ1人 × 定番に似た名前」が重なったら、そこは立ち止まる。信号は掛け算で見ます。

人間とAIの役割分担表

工程 AIに任せる 人間 / CIが握る
名前の提案 ◎ 候補を出す
実在の確認 補助(表の下書き) ◎ レジストリ照合で最終判定
素性の評価 ◎ 材料を並べる ◎ 許可リスト入りの承認
installの実行 ◎ 実行 / lock更新の承認
監査の定例化 ◎ レポート生成 ◎ しきい値と撤退の判断

撤退ライン表 — こうなったら、いったん止める

症状 危険な理由 対応
レジストリに404 幻覚か未登録 installしない。名前を疑う
公開が数日 × DLごく少 新規のslopsquatかも 保留。実績を待つ / 別を探す
定番と1文字違い typosquat / conflation 正しい正規名に置き換える
postinstallで外部通信 隠れた実行がある --ignore-scripts。中身を確認

明日からの最初の一歩(4ステップ)

いきなり全部やらなくていいです。軸は「明日の自分がちょっと助かる、小さな一歩」。

  1. 今開いているプロジェクトの新規依存を1個だけ、PyPI か npm のページで開いて、実在と公開日を自分の目で見てみる(30秒)
  2. コード1かコード2を保存して、手元の依存名を渡して一回回してみる
  3. AIへの生成プロンプトに、プロンプト1の「検証テーブルを付けて」を足す
  4. CIに dep-guard を1本足す(コード3)。最初は止めずに警告からで十分

この4つのうち、今日は1個でいいと思います。1個やっておくだけで、明日の自分が「お、確かめといてくれたんや、あざっす」って言ってくれる。それで十分なんです。


おわりに — 30秒の検証は、未来の自分へのプレゼント

ここまで読んでくださって、ありがとうございます。

僕がこの話でいちばん伝えたかったのは、「AIは危ないから使うな」じゃないんです。むしろ逆で、AIにどんどん任せたいからこそ、任せる範囲の外側に、静かな柵を一本引いておこう、という話でした。

依存を1個確かめるのって、たった30秒くらいの、地味な作業です。でも、その30秒をサボった未来に待っているのは、本番で情報が漏れたり、深夜にインシデント対応で青くなったりする自分かもしれない。だとしたら、今日の30秒は、責められて渋々やる作業じゃなくて、未来の自分にそっと渡しておくプレゼントなんですよね。

僕はいつも、選択に迷ったら「これ、明日の自分があざっすって言ってくれる方はどっちかな」って考えるようにしています。依存の検証は、まさにそれ。派手さはないけど、確実に明日の自分が助かる、思いやりの一手だと思います。

それに、もし自分でアプリやサービスを作って世に出していくなら、「あの人が作ったものなら安心して使える」という信頼こそが、いちばんの資産になります。依存の素性を静かに管理しておくことは、その信頼の土台そのもの。かっこいいプロダクトは、こういう地味な足元から作られていくんだと思います。

AIを止めるんじゃなくて、AIに安心して背中を預けられる柵を作る。今日の1個の検証から、ゆるっと始めてみませんか。

明日の自分に、あざっす。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?