С широким внедрением автономных агентов кодогенерации (Devin, Cursor, Copilot Workspace, внутренняя AI-автоматизация) вектор атак на цепочку поставок программного обеспечения (Software Supply Chain) претерпел качественную трансформацию. Если классический Typosquatting эксплуатирует случайные опечатки живого разработчика при вводе имени пакета (например, lodahs вместо lodash), то состязательный вектор Slopsquatting (также известный как AI Package Hallucination Exploitation) эксплуатирует фундаментальные свойства авторегрессионных языковых моделей.
Большие языковые модели обучаются на миллиардах строк кода и генерируют зависимости на основе семантической ассоциативности токенов. При запросе нестандартного функционала модель генерирует правдоподобное, синтетически безупречное имя библиотеки (например, @company-tools/fast-id-generator, react-virtual-waterfall-pro, wp-query-caching-layer), которого физически никогда не существовало в открытых реестрах (npm, Packagist, PyPI).
В автономных пайплайнах агент самостоятельно правит манифесты (package.json, composer.json), формирует ветку и запускает установку зависимостей в CI/CD. Вредоносный payload в момент вызова менеджера пакетов получает доступ к переменным окружения CI-раннера, SSH-ключам развертывания и приватным репозиториям.
Защита от Slopsquatting строится на детерминированном барьере между генеративным кодом агента и системным пакетным менеджером. Ни одна сгенерированная зависимость не может быть установлена без предварительного аудита через реестровые API и шлюзы репутации.
[AI-Agent PR: Новые зависимости в package.json / composer.json]
│
▼
┌───────────────────────────┐
│ Git Diff Manifest Parser │ ──(Извлечение новых пакетов)
└───────────────────────────┘
│
▼
┌───────────────────────────┐
│ Package Security Gateway │
└───────────────────────────┘
│ │ │
(Registry Existence) (Age & Velocity) (Reputation & AST)
│ │ │
npm / Packagist < 30 дней Socket.dev API
HTTP 404 = Block Downloads < 1k Install Scripts = Block
│ │ │
└───────────┬───────────┘
│
[Passed Security Gate?]
├──(NO)──> [CI Failure: Alert Security Team & Abort]
└──(YES)─> [pnpm install --ignore-scripts (Sandbox)]--ignore-scripts), полностью нейтрализующее исполнение кода из секций preinstall и postinstall.
package.json несуществующий пакет next-image-optimizer-turbo. Шлюз CI/CD блокирует установку до момента подтверждения человеком.composer.json для интеграции с внешними REST API. Шлюз проверяет Packagist API на отсутствие заброшенных форков и вредоносных подмен вендоров.Ниже представлена реализация автономного шлюза проверки зависимостей на TypeScript. Скрипт анализирует изменения в package.json и composer.json, делает параллельные запросы к API официальных реестров и блокирует сборку при обнаружении признаков Slopsquatting.
import { z } from 'zod';
// Схема ответа NPM Registry API
const NpmRegistryResponseSchema = z.object({
name: z.string(),
'dist-tags': z.record(z.string(), z.string()),
time: z.record(z.string(), z.string()),
versions: z.record(
z.string(),
z.object({
scripts: z.record(z.string(), z.string()).optional(),
}).optional()
),
});
// Схема ответа Packagist API
const PackagistResponseSchema = z.object({
packages: z.record(
z.string(),
z.array(
z.object({
name: z.string(),
time: z.string(),
})
)
),
});
interface DependencyCheckResult {
packageName: string;
ecosystem: 'npm' | 'packagist';
valid: boolean;
reasons: string[];
}
export class SupplyChainVerificationGateway {
private readonly minAgeDays: number = 14;
private readonly allowedNpmPackages: Set<string>;
constructor(allowedNpmPackagesList: string[] = []) {
this.allowedNpmPackages = new Set(allowedNpmPackagesList);
}
/**
* Проверка пакета в npm registry
*/
public async verifyNpmPackage(packageName: string): Promise<DependencyCheckResult> {
// 1. Проверка по корпоративному Whitelist
if (this.allowedNpmPackages.has(packageName)) {
return { packageName, ecosystem: 'npm', valid: true, reasons: ['Whitelisted package'] };
}
const reasons: string[] = [];
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
try {
const response = await fetch(url, {
headers: { Accept: 'application/json' },
});
// Если пакета нет в реестре, но агент попытался его установить
if (response.status === 404) {
return {
packageName,
ecosystem: 'npm',
valid: false,
reasons: ['Package does not exist in registry (Classic Slopsquatting Hallucination).'],
};
}
if (!response.ok) {
throw new Error(`NPM Registry responded with status ${response.status}`);
}
const rawData = await response.json();
const pkgData = NpmRegistryResponseSchema.parse(rawData);
// 2. Проверка возраста пакета (молодые пакеты — высокий риск регистрации злоумышленником)
const createdTime = new Date(pkgData.time.created);
const ageInDays = (Date.now() - createdTime.getTime()) / (1000 * 60 * 60 * 24);
if (ageInDays < this.minAgeDays) {
reasons.push(`Suspicious package age: created ${ageInDays.toFixed(1)} days ago (min required: ${this.minAgeDays}).`);
}
// 3. Проверка наличия postinstall скриптов (RCE вектор)
const latestVersion = pkgData['dist-tags'].latest;
const latestMeta = pkgData.versions[latestVersion];
if (latestMeta?.scripts) {
const dangerousHooks = ['preinstall', 'install', 'postinstall'];
const hasHook = dangerousHooks.some((hook) => hook in (latestMeta.scripts || {}));
if (hasHook) {
reasons.push('Package contains lifecycle execution scripts (preinstall/install/postinstall).');
}
}
return {
packageName,
ecosystem: 'npm',
valid: reasons.length === 0,
reasons,
};
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Unknown registry error';
return {
packageName,
ecosystem: 'npm',
valid: false,
reasons: [`Registry verification error: ${msg}`],
};
}
}
/**
* Проверка пакета в Packagist (Composer / WordPress экосистема)
*/
public async verifyPackagistPackage(vendorPackage: string): Promise<DependencyCheckResult> {
const reasons: string[] = [];
const url = `https://repo.packagist.org/p2/${vendorPackage}.json`;
try {
const response = await fetch(url);
if (response.status === 404) {
return {
packageName: vendorPackage,
ecosystem: 'packagist',
valid: false,
reasons: ['Packagist vendor/package not found (Hallucinated PHP dependency).'],
};
}
if (!response.ok) {
throw new Error(`Packagist returned HTTP ${response.status}`);
}
const raw = await response.json();
const parsed = PackagistResponseSchema.parse(raw);
if (!parsed.packages[vendorPackage] || parsed.packages[vendorPackage].length === 0) {
reasons.push('No release versions registered in Packagist repository.');
}
return {
packageName: vendorPackage,
ecosystem: 'packagist',
valid: reasons.length === 0,
reasons,
};
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Packagist error';
return {
packageName: vendorPackage,
ecosystem: 'packagist',
valid: false,
reasons: [`Packagist verification failure: ${msg}`],
};
}
}
}Для интеграции в пайплайн GitHub Actions настраивается гейт блокировки, который запускается перед сборкой:
name: Supply Chain Slopsquatting Shield
on:
pull_request:
paths:
- 'package.json'
- 'pnpm-lock.yaml'
- 'composer.json'
jobs:
verify-dependencies:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Run Slopsquatting & Lifecycle Verification
run: |
npx tsx scripts/verify-supply-chain.ts
- name: Secure pnpm Install
run: |
# Флаг --ignore-scripts гарантирует, что lifecycle-скрипты не запустятся в CI
pnpm install --frozen-lockfile --ignore-scriptsЗапуск команды npm install или pnpm install по умолчанию исполняет хуки preinstall и postinstall. Если злоумышленник успел зарегистрировать сгаллюцинированное имя пакета и поместил в package.json вызов node setup.js, вредоносный код выполнится в момент инсталляции до запуска тестов, скомпрометировав ключи GitHub Actions.
Разрешение агенту прописывать зависимости со спецсимволами вроде ^1.0.0 или *. Злоумышленник, зарегистрировав библиотеку-ловушку, может выпустить версию 1.0.1 с эксплойтом, которая автоматически подтянется раннером при очередной сборке. Для сгенерированного кода допустимы только строго зафиксированные (pinned) версии с lockfile-хэшами.
Считать пакет безопасным исключительно потому, что реестр вернул код 200. В сценарии Slopsquatting хакер как раз регистрирует валидный пакет с галлюцинированным именем. Без проверки даты регистрации (Package Age), динамики скачиваний и сигнатур Socket.dev такая проверка бесполезна.
Агент в PR коммитит одновременно package.json и pnpm-lock.yaml / composer.lock. Если lock-файл сгенерирован внутри незащищенного окружения модели, значения integrity (sha512 хэши) могут ссылаться на сторонние tarball-архивы, размещенные на внешних серверах атакующего. Lockfile всегда должен генерироваться доверенным детерминированным раннером CI.
Анализатор выявляет все добавленные или модифицированные библиотеки в package.json и composer.json.
Проверка наличия пакета в официальных реестрах. Выявление галлюцинаций первого порядка (404 Not Found).
Анализ метаданных: дата создания (менее 14 дней), количество скачиваний, наличие привязанного Git-репозитория.
Проверка исходного кода пакета на наличие сетевых сокетов, RCE-функций (child_process, exec) и postinstall-хуков.
Установка зависимостей с флагом --ignore-scripts внутри изолированного контейнера без прав root.