はじめに — 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 installやnpm 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つ
怖い仕組みがわかったところで、じゃあ実際どこで刺さるのか。開発フローの中で危ないポイントを、正直に洗い出しておきましょう。敵の顔を見ておくと、守る場所が決まります。
-
人間のコピペ実行 … AIの回答にあった
pip install ○○を、名前を確認せずそのままターミナルへ。いちばん古典的で、いちばん多い。 -
AIエージェントの自動install … 最近はAIが自分でコマンドを実行する。人間が見ていないところで
installまで走ると、幻覚名がそのまま入る。 -
依存ファイルの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個だけ、PyPI か npm のページで開いて、実在と公開日を自分の目で見てみる(30秒)
- コード1かコード2を保存して、手元の依存名を渡して一回回してみる
- AIへの生成プロンプトに、プロンプト1の「検証テーブルを付けて」を足す
- CIに
dep-guardを1本足す(コード3)。最初は止めずに警告からで十分
この4つのうち、今日は1個でいいと思います。1個やっておくだけで、明日の自分が「お、確かめといてくれたんや、あざっす」って言ってくれる。それで十分なんです。
おわりに — 30秒の検証は、未来の自分へのプレゼント
ここまで読んでくださって、ありがとうございます。
僕がこの話でいちばん伝えたかったのは、「AIは危ないから使うな」じゃないんです。むしろ逆で、AIにどんどん任せたいからこそ、任せる範囲の外側に、静かな柵を一本引いておこう、という話でした。
依存を1個確かめるのって、たった30秒くらいの、地味な作業です。でも、その30秒をサボった未来に待っているのは、本番で情報が漏れたり、深夜にインシデント対応で青くなったりする自分かもしれない。だとしたら、今日の30秒は、責められて渋々やる作業じゃなくて、未来の自分にそっと渡しておくプレゼントなんですよね。
僕はいつも、選択に迷ったら「これ、明日の自分があざっすって言ってくれる方はどっちかな」って考えるようにしています。依存の検証は、まさにそれ。派手さはないけど、確実に明日の自分が助かる、思いやりの一手だと思います。
それに、もし自分でアプリやサービスを作って世に出していくなら、「あの人が作ったものなら安心して使える」という信頼こそが、いちばんの資産になります。依存の素性を静かに管理しておくことは、その信頼の土台そのもの。かっこいいプロダクトは、こういう地味な足元から作られていくんだと思います。
AIを止めるんじゃなくて、AIに安心して背中を預けられる柵を作る。今日の1個の検証から、ゆるっと始めてみませんか。
明日の自分に、あざっす。