daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

dependency-confusion.md (20464B)


      1 ---
      2 title: "Dependency Confusion"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/dependency-confusion.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/dependency-confusion.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Dependency Confusion
     14 
     15 ## Basic Information
     16 
     17 Dependency Confusion (a.k.a. substitution attacks) happens when a package manager resolves a dependency name from an unintended, less-trusted registry/source (usually a public registry) instead of the intended private/internal one. This typically leads to the installation of an attacker-controlled package.
     18 
     19 Common root causes:
     20 - Typosquatting/misspelling: Importing `reqests` instead of `requests` (resolves from public registry).
     21 - Non-existent/abandoned internal package: Importing `company-logging` that no longer exists internally, so the resolver looks in public registries and finds an attacker’s package.
     22 - Version preference across multiple registries: Importing an internal `company-requests` while the resolver is allowed to also query public registries and prefers the “best”/newer version published publicly by an attacker.
     23 
     24 Key idea: If the resolver can see multiple registries for the same package name and is allowed to pick the “best” candidate globally, you’re vulnerable unless you constrain resolution.
     25 
     26 
     27 ## Exploitation
     28 
     29 > [!WARNING]
     30 > In all cases, the attacker only needs to publish a malicious package with the same name as the dependency your build resolves from a public registry. Installation-time hooks (e.g., npm scripts) or import-time code paths often give code execution.
     31 
     32 ### Misspelled & Inexistent
     33 
     34 If your project references a library that isn’t available in the private registry, and your tooling falls back to a public registry, an attacker can seed a malicious package with that name in the public registry. Your runners/CI/dev machines will fetch and execute it.
     35 
     36 ### Unspecified Version / “Best-version” selection across indexes
     37 
     38 Developers frequently leave versions unpinned or allow wide ranges. When a resolver is configured with both internal and public indexes, it may select the newest version regardless of source. For internal names like `requests-company`, if the internal index has `1.0.1` but an attacker publishes `1.0.2` to the public registry and your resolver considers both, the public package may win.
     39 
     40 ### Related pattern: compromise of a legitimate package release
     41 
     42 Dependency confusion is not the only way to get install-time execution. If an attacker compromises the maintainer account or publishing token of a legitimate package, they can publish a malicious version of the real package and obtain code execution on every machine that installs it.<sup>[[5]](#references)</sup>
     43 
     44 Common pattern in the npm ecosystem:
     45 - The attacker modifies only `package.json` and adds a new dependency.
     46 - The new dependency is **never imported** by the main library, so source review of the application code may look clean.
     47 - The dependency contains a `preinstall`/`install`/`postinstall` hook that runs automatically during `npm install`, `npm ci`, Yarn, pnpm, or CI builds.
     48 - The hook fetches or drops the real payload, often choosing per-OS implants for macOS, Windows, and Linux.
     49 
     50 This is a useful red-team and incident-response mental model because **importing the victim library is not required**. The execution path is installation-time, not runtime.
     51 
     52 Minimal malicious pattern:
     53 
     54 `package.json` of the compromised package:
     55 ```json
     56 {
     57   "name": "popular-lib",
     58   "version": "1.2.4",
     59   "dependencies": {
     60     "helper-lib": "^4.2.1"
     61   }
     62 }
     63 ```
     64 
     65 `package.json` of the injected dependency:
     66 ```json
     67 {
     68   "name": "helper-lib",
     69   "version": "4.2.1",
     70   "scripts": {
     71     "postinstall": "node setup.js"
     72   }
     73 }
     74 ```
     75 
     76 Practical notes:
     77 - CI/CD runners are especially valuable targets because the hook runs before tests and often has access to cloud credentials, package tokens, signing keys, and deployment secrets.
     78 - If you are trying to prove impact in an authorized exercise, `postinstall` is usually enough to demonstrate install-time code execution without modifying application logic.
     79 - Defenders should remember that `npm ci` still runs lifecycle scripts unless `--ignore-scripts` is set.
     80 
     81 ### Obfuscated Node.js droppers and manifest laundering
     82 
     83 Malicious install hooks often try to survive quick review by:<sup>[[5]](#references)</sup>
     84 - Hiding C2 strings or commands behind layered transforms such as reversed Base64, XOR, or split strings.
     85 - Dynamically loading Node modules (`fs`, `os`, `child_process`, `execSync`) only at runtime to reduce obvious static indicators.
     86 - Deleting the dropper after execution and restoring a benign-looking manifest.
     87 
     88 One anti-forensic trick is **manifest laundering**:
     89 1. Run the malicious hook.
     90 2. Delete the malicious `setup.js` or equivalent.
     91 3. Delete the malicious `package.json`.
     92 4. Rename a benign stub such as `package.md` back to `package.json`.
     93 
     94 After infection, the installed dependency directory may look clean unless investigators review install logs, lockfile changes, registry metadata, package tarballs, file timelines, or known-good hashes.
     95 
     96 ### `npx` binary-to-package confusion
     97 
     98 `npx <name>` is a **different supply-chain primitive** from classic dependency confusion. If no explicit `--package` is given, npm first tries to resolve `<name>` as an executable from the local `node_modules/.bin`, global bins, installed package trees, and the `npx` cache. If none of those checks succeed, npm treats the same string as a **package name**, installs it into `~/.npm/_npx/<hash>/`, prepends `node_modules/.bin` from that cache entry to `PATH`, and executes it.<sup>[[9]](#references)</sup><sup>[[10]](#references)</sup><sup>[[11]](#references)</sup>
     99 
    100 That means an attacker can win with **binary-name takeover** even when there is no public/private registry precedence issue:
    101 
    102 1. Discover scripts, READMEs, CI jobs, bundles, or leaked `package.json` files containing `npx <binary_name>`.
    103 2. Confirm the real package exposing that binary has a different package name (very common with scoped packages such as `@company/tool`).
    104 3. Check whether the public package `<binary_name>` is unclaimed.
    105 4. Publish `<binary_name>` with a matching `bin` entry.
    106 5. Wait for a developer, CI runner, automation job, or agent to execute `npx <binary_name>` **outside the correct dependency context** (wrong cwd, fresh workspace, dependencies not installed, minimal container, etc.).
    107 
    108 Why scoped packages are dangerous here:
    109 - The real package can be `@company/tool`, but its executable is usually unscoped (`tool`, `build`, `sync-assets`, etc.).
    110 - If `npx tool` cannot find the local binary, npm may fetch the public package `tool` instead of the intended private/scoped package.
    111 - In non-interactive contexts, npm assumes `--yes`, so CI/automation often auto-installs the missing package with only a warning in logs.
    112 
    113 Practical red-team checks:
    114 - Grep for `npx ` in repositories, CI definitions, docs, shell history, bundled `package.json`, and transpiled JavaScript.
    115 - Enumerate `bin` entries from internal packages and compare the executable names against the actual package names.
    116 - Inspect `~/.npm/_npx/` on build agents or developer workstations for unexpected cached packages and `_npx` metadata.
    117 - Review CI logs for messages like `The following package was not found and will be installed`.
    118 
    119 Practical hardening:
    120 - Prefer `npx --package <expected-package> <binary>` (or `npm exec --package=<expected-package> -- <binary>`) so the binary is bound to the intended package.
    121 - Reserve public names matching internal binaries, especially names exported by scoped/private packages.
    122 - In sensitive runners, use `--no`, `offline`, or preinstall the expected package instead of allowing implicit remote fetches.
    123 - Run automation from the intended project root and fail closed when `node_modules/.bin/<binary>` is absent.
    124 
    125 
    126 ## AWS Fix
    127 
    128 This vulnerability was found in AWS CodeArtifact (read the details in this blog post). AWS added controls to mark dependencies/feeds as internal vs external so the client won’t fetch “internal” names from upstream public registries.<sup>[[2]](#references)</sup>
    129 
    130 
    131 ## Finding Vulnerable Libraries
    132 
    133 In the original post about dependency confusion the author looked for thousands of exposed manifests (e.g., `package.json`, `requirements.txt`, lockfiles) to infer internal package names and then published higher-versioned packages to public registries.<sup>[[1]](#references)</sup>
    134 
    135 
    136 ## Practical Attacker Playbook (for red teams in authorized tests)
    137 
    138 - Enumerate names:
    139   - Grep repos and CI configs for manifest/lock files and internal namespaces.
    140   - Look for organization-specific prefixes (e.g., `@company/*`, `company-*`, internal groupIds, NuGet ID patterns, private module paths for Go, etc.).
    141 - Check public registries for availability:
    142   - If the name is unregistered publicly, register it; if it exists, attempt subdependency hijacking by targeting internal transitive names.
    143 - Publish with precedence:
    144   - Choose a semver that “wins” (e.g., a very high version) or matches resolver rules.
    145   - Include minimal install-time execution where applicable (e.g., npm `preinstall`/`install`/`postinstall` scripts). For Python, prefer import-time execution paths, as wheels typically don’t execute arbitrary code on install.
    146 - Exfil control:
    147   - Ensure outbound is allowed from CI to your controlled endpoint; otherwise use DNS queries or error messages as a side-channel to prove code execution.
    148 
    149 > [!CAUTION]
    150 > Always get written authorization, use unique package names/versions for the engagement, and immediately unpublish or coordinate cleanup when testing concludes.
    151 
    152 
    153 ## Defender Playbook (what actually prevents confusion)
    154 
    155 High-level strategies that work across ecosystems:
    156 - Use unique internal namespaces and bind them to a single registry.
    157 - Avoid mixing trust levels at resolution time. Prefer a single internal registry that proxies approved public packages instead of giving package managers both internal and public endpoints.
    158 - For managers that support it, map packages to specific sources (no global “best-version” across registries).
    159 - Pin and lock:
    160   - Use lockfiles that record the resolved registry URLs (npm/yarn/pnpm) or use hash/attestation pinning (pip `--require-hashes`, Gradle dependency verification).
    161 - Block public fallback for internal names at the registry/network layer.
    162 - Reserve your internal names in public registries when feasible to prevent future squat.
    163 
    164 
    165 ## Ecosystem Notes and Secure Config Snippets
    166 
    167 Below are pragmatic, minimal configs to reduce or eliminate dependency confusion. Prefer enforcing these in CI and developer environments.
    168 
    169 ### JavaScript/TypeScript (npm, Yarn, pnpm)
    170 
    171 - Use scoped packages for all internal code and pin the scope to your private registry.
    172 - Keep installs immutable in CI (npm lockfile, `yarn install --immutable`).
    173 
    174 .npmrc (project-level)
    175 ```text
    176 # Bind internal scope to private registry; do not allow public fallback for @company/*
    177 @company:registry=https://registry.corp.example/npm/
    178 # Always authenticate to the private registry
    179 //registry.corp.example/npm/:_authToken=${NPM_TOKEN}
    180 strict-ssl=true
    181 ```
    182 
    183 package.json (for internal package)
    184 ```text
    185 {
    186   "name": "@company/api-client",
    187   "version": "1.2.3",
    188   "private": false,
    189   "publishConfig": {
    190     "registry": "https://registry.corp.example/npm/",
    191     "access": "restricted"
    192   }
    193 }
    194 ```
    195 
    196 Yarn Berry (.yarnrc.yml)<sup>[[4]](#references)</sup>
    197 ```text
    198 npmScopes:
    199   company:
    200     npmRegistryServer: "https://registry.corp.example/npm/"
    201     npmAlwaysAuth: true
    202 # CI should fail if lockfile would change
    203 enableImmutableInstalls: true
    204 ```
    205 
    206 Operational tips:
    207 - Only publish internal packages within the `@company` scope.
    208 - For third-party packages, allow public registry via your private proxy/mirror, not directly from clients.
    209 - Consider enabling npm package provenance for public packages you publish to increase traceability (doesn’t by itself prevent confusion).
    210 - For high-risk environments, install with scripts disabled first (`npm ci --ignore-scripts`) and only allow scripts in controlled build stages.
    211 
    212 ### Python (pip / Poetry)
    213 
    214 Core rule: Don’t use `--extra-index-url` to mix trust levels. Either:
    215 - Expose a single internal index that proxies and caches approved PyPI packages, or
    216 - Use explicit index selection and hash pinning.
    217 
    218 pip.conf
    219 ```text
    220 [global]
    221 index-url = https://pypi.corp.example/simple
    222 # Disallow source distributions when possible
    223 only-binary = :all:
    224 # Lock with hashes generated via pip-tools
    225 require-hashes = true
    226 ```
    227 
    228 Generate hashed requirements with pip-tools:
    229 ```text
    230 # From pyproject.toml or requirements.in
    231 pip-compile --generate-hashes -o requirements.txt
    232 pip install --require-hashes -r requirements.txt
    233 ```
    234 
    235 If you must reach public PyPI, do it via your internal proxy and maintain an explicit allowlist there. Avoid `--extra-index-url` in CI.
    236 
    237 ### .NET (NuGet)
    238 
    239 Use Package Source Mapping to tie package ID patterns to explicit sources and prevent resolution from unexpected feeds.<sup>[[3]](#references)</sup>
    240 
    241 nuget.config
    242 ```text
    243 <?xml version="1.0" encoding="utf-8"?>
    244 <configuration>
    245   <packageSources>
    246     <clear />
    247     <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
    248     <add key="corp" value="https://nuget.corp.example/v3/index.json" />
    249   </packageSources>
    250   <packageSourceMapping>
    251     <packageSource key="nuget.org">
    252       <package pattern="*" />
    253     </packageSource>
    254     <packageSource key="corp">
    255       <package pattern="Company.*" />
    256       <package pattern="Internal.Utilities" />
    257     </packageSource>
    258   </packageSourceMapping>
    259 </configuration>
    260 ```
    261 
    262 ### Java (Maven/Gradle)
    263 
    264 Maven settings.xml (mirror all to internal; disallow ad-hoc repos in POMs via Enforcer):
    265 ```text
    266 <settings>
    267   <mirrors>
    268     <mirror>
    269       <id>internal-mirror</id>
    270       <mirrorOf>*</mirrorOf>
    271       <url>https://maven.corp.example/repository/group</url>
    272     </mirror>
    273   </mirrors>
    274 </settings>
    275 ```
    276 
    277 Add Enforcer to ban repositories declared in POMs and force usage of your mirror:
    278 ```text
    279 <plugin>
    280   <groupId>org.apache.maven.plugins</groupId>
    281   <artifactId>maven-enforcer-plugin</artifactId>
    282   <version>3.6.1</version>
    283   <executions>
    284     <execution>
    285       <id>enforce-no-repositories</id>
    286       <goals><goal>enforce</goal></goals>
    287       <configuration>
    288         <rules>
    289           <requireNoRepositories />
    290         </rules>
    291       </configuration>
    292     </execution>
    293   </executions>
    294 </plugin>
    295 ```
    296 
    297 Gradle: Centralize and lock dependencies.
    298 - Enforce repositories in `settings.gradle(.kts)` only:
    299 ```text
    300 dependencyResolutionManagement {
    301   repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    302   repositories {
    303     maven { url = uri("https://maven.corp.example/repository/group") }
    304   }
    305 }
    306 ```
    307 - Enable dependency verification (checksums/signatures) and commit `gradle/verification-metadata.xml`.
    308 
    309 ### Go Modules
    310 
    311 Configure private modules so the public proxy and checksum DB aren’t used for them.
    312 
    313 ```text
    314 # Use corporate proxy first, then public proxy as fallback
    315 export GOPROXY=https://goproxy.corp.example,https://proxy.golang.org
    316 # Mark private paths to skip proxy and checksum db
    317 export GOPRIVATE=*.corp.example.com,github.com/your-org/*
    318 export GONOSUMDB=*.corp.example.com,github.com/your-org/*
    319 ```
    320 
    321 ### Rust (Cargo)
    322 
    323 Replace crates.io with an approved internal mirror or vendor directory for builds; do not allow arbitrary public fallback.
    324 
    325 .cargo/config.toml
    326 ```text
    327 [source.crates-io]
    328 replace-with = "corp-mirror"
    329 
    330 [source.corp-mirror]
    331 registry = "https://crates-mirror.corp.example/index"
    332 ```
    333 
    334 For publishing, be explicit with `--registry` and keep credentials scoped to the target registry.
    335 
    336 ### Ruby (Bundler)
    337 
    338 Use source blocks and disable multisource Gemfiles so gems come only from the intended repository.
    339 
    340 Gemfile
    341 ```text
    342 source "https://gems.corp.example"
    343 
    344 source "https://rubygems.org" do
    345   gem "rails"
    346   gem "pg"
    347 end
    348 
    349 source "https://gems.corp.example" do
    350   gem "company-logging"
    351 end
    352 ```
    353 
    354 Enforce at config level:
    355 ```text
    356 bundle config set disable_multisource true
    357 ```
    358 
    359 
    360 ## CI/CD and Registry Controls That Help
    361 
    362 - Private registry as a single ingress:
    363   - Use Artifactory/Nexus/CodeArtifact/GitHub Packages/Azure Artifacts as the only endpoint developers/CI can reach.
    364   - Implement block/allow rules so internal namespaces never resolve from upstream public sources.
    365 - Lockfiles are immutable in CI:
    366   - npm: commit `package-lock.json`, use `npm ci`.
    367   - Yarn: commit `yarn.lock`, use `yarn install --immutable`.
    368   - Python: commit hashed `requirements.txt`, enforce `--require-hashes`.
    369   - Gradle: commit `verification-metadata.xml` and fail on unknown artifacts.
    370 - Outbound egress control: block direct access from CI to public registries except via the approved proxy.
    371 - Name reservation: pre-register your internal names/namespaces in public registries where supported.
    372 - Package provenance / attestations: when publishing public packages, enable provenance/attestations to make tampering more detectable downstream.<sup>[[7]](#references)</sup>
    373 
    374 ### Detecting unauthorized publishes in trusted-publisher pipelines
    375 
    376 If a package normally uses npm trusted publishing with GitHub Actions or GitLab OIDC, a release pushed with a stolen classic token often looks different from legitimate releases.<sup>[[6]](#references)</sup>
    377 
    378 Useful heuristics:
    379 - The package version exists in the registry but lacks the expected trusted-publisher / provenance metadata.
    380 - There is no matching git tag or release commit for the published version.
    381 - The package tarball adds a dependency whose only purpose is a lifecycle hook.
    382 - The newly added dependency is never referenced by the main library source.
    383 - The lockfile or install logs show `preinstall` / `postinstall` execution shortly before network egress or secret access from a runner.
    384 
    385 This is not limited to dependency confusion: it also catches compromise of maintainer credentials or leaked automation tokens.
    386 
    387 ### Cooldown / age-gate controls for fresh releases
    388 
    389 Fresh malicious versions are often detected and removed quickly. Delaying adoption of newly published versions can block a large class of opportunistic supply-chain compromises:<sup>[[8]](#references)</sup><sup>[[12]](#references)</sup><sup>[[13]](#references)</sup>
    390 
    391 ```yaml
    392 # pnpm-workspace.yaml
    393 minimumReleaseAge: 10080 # 7 days in minutes
    394 ```
    395 
    396 ```yaml
    397 # .yarnrc.yml
    398 npmMinimalAgeGate: "7d"
    399 ```
    400 
    401 ```toml
    402 # bunfig.toml
    403 [install]
    404 minimumReleaseAge = 604800 # 7 days in seconds
    405 ```
    406 
    407 ```ini
    408 # .npmrc
    409 min-release-age=7
    410 ```
    411 
    412 These controls do **not** replace lockfiles or trusted publishing, but they reduce exposure to packages published minutes or hours earlier.
    413 
    414 
    415 ## References
    416 
    417 - [1] [Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies](https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610)
    418 - [2] [Dependency confusion in AWS CodeArtifact](https://zego.engineering/dependency-confusion-in-aws-codeartifact-86b9ff68963d)
    419 - [3] [NuGet Package Source Mapping - Microsoft Learn](https://learn.microsoft.com/en-us/nuget/consume-packages/package-source-mapping)
    420 - [4] [Yarn - .yarnrc.yml configuration reference](https://yarnpkg.com/configuration/yarnrc/)
    421 - [5] [Frequently Asked Questions About the Axios npm Supply Chain Attack by North Korea-Nexus Threat Actor UNC1069 - Tenable](https://www.tenable.com/blog/faq-about-the-axios-npm-supply-chain-attack-by-north-korea-nexus-threat-actor-unc1069)
    422 - [6] [About trusted publishers - npm Docs](https://docs.npmjs.com/trusted-publishers/)
    423 - [7] [Generating provenance statements - npm Docs](https://docs.npmjs.com/generating-provenance-statements)
    424 - [8] [npm CLI v11 changelog](https://docs.npmjs.com/cli/v11/using-npm/changelog/)
    425 - [9] [npm exec command reference - npm Docs](https://docs.npmjs.com/cli/v11/commands/npm-exec/)
    426 - [10] [npx Used Confusion and It's Super Effective](https://www.landh.tech/blog/20260521-npx-used-confusion-and-its-super-effective)
    427 - [11] [npx Confusion: Packages That Forgot to Claim Their Own Name](https://www.aikido.dev/blog/npx-confusion-unclaimed-package-names)
    428 - [12] [pnpm Settings reference (minimumReleaseAge)](https://pnpm.io/settings)
    429 - [13] [Bun bunfig.toml runtime configuration reference](https://bun.sh/docs/runtime/bunfig)