Black Hat Asia 2026 (Singapore, Apr 23–24) — Briefings Deep Dives

Hack the Source, Of the Source

Talk 17 · Supply chain security · analysis from the speaker's published deck

Sep 24, 2026 · conferences · 11 min read · 2241 words advanced

Hack the source, of the source — when the registry itself is the supply chain.

conferences blackhat supply-chain package-registries appsec

Official recording, Black Hat YouTube channel · open on YouTube ↗

Tsi-Lin "splitline" Ng pushes the software supply-chain conversation up a level. The standard advice, use well-known package sources, install from trusted developers, don't typo a package name, all assumes the registry itself (PyPI, npm, LuaRocks, NuGet, and their peers) is a solid foundation to trust. Ng's question: what if the foundation itself has bugs? Rather than publishing a malicious package (the well-studied "malicious upload" supply-chain vector), this research targets the registry's own backend processing logic, the code that generates manifests, resolves versions from source control, selects metadata files out of an uploaded archive, and verifies publisher identity. Across four ecosystems, LuaRocks (Lua), JuliaHub (Julia), NuGet (C#/.NET), and CocoaPods (Objective-C/Swift), the same category of mistake recurred: backend logic trusted attacker-controlled input (a URL, a filename, a version string, a registration timing window) more than it should have, in ways that let an attacker poison what every downstream consumer of a totally legitimate-looking package would receive. All findings shown were responsibly reported and are already fixed.

Post 17 of my Black Hat Asia 2026 series. Every vulnerability here has already been disclosed and patched; I describe the classes of bug and their impact, not the exact injection syntax or reproduction steps, those live in the original vendor advisories.

1. Reframing the supply-chain threat model

Most supply-chain security advice targets one specific threat: a malicious actor uploads or takes over a package (typosquatting, dependency confusion, a compromised maintainer account) and legitimate infrastructure faithfully distributes it. That threat model implicitly treats the registry, the servers that receive uploads, store them, generate metadata, and serve them back to millions of installers, as a neutral, trustworthy pipe.

Ng's research tests that assumption directly: package registries are themselves complex, internet-facing software systems, built by fallible engineers, under the same kinds of deadline and legacy-code pressure as anything else. If the registry's own processing pipeline can be tricked, an attacker doesn't need to compromise any individual developer or account at all, they can make the registry itself serve poisoned content for an otherwise completely legitimate package, to every single person who ever installs it.

Two different supply-chain threat models Classic: malicious package Attacker → uploads evil package Registry → faithfully distributes it Defense: vet what you install This talk: registry logic bug Attacker → exploits backend processing Registry → serves poisoned metadata for an otherwise trusted package

Fig. 1 — "Only install from trusted sources" doesn't help when the trusted source's own backend can be made to lie.

2. LuaRocks: version data and manifest generation

LuaRocks is the package manager for Lua, widely used in configurations like OpenResty/ngx_lua and Neovim plugin ecosystems. Its central manifest file describes, for every package name, which versions exist and where their metadata (a "rockspec" file) and source archive can be found. Ng's research examined how that manifest is generated and validated, specifically, whether the fields describing a package's version and architecture were treated as trusted structural data or as attacker-influenceable strings.

Because rockspec files are themselves Lua-syntax data structures, and because manifest generation involves composing and re-parsing pieces of that syntax, the boundary between "a legitimate package's declared metadata" and "a string an attacker can shape" turned out to be thinner than it should have been in one specific processing path. Separately, the research also touched on a numeric-precision quirk (large integers behaving differently between a Lua runtime using doubles versus one using native 64-bit integers), a classic type-confusion-adjacent inconsistency where the same version identifier could be interpreted differently by the server generating the manifest than by the client consuming it, exactly the kind of client/server disagreement that shows up repeatedly across this series (see also the Windows auth-reflection Unicode bypass) as a general pattern: whenever two components must independently agree on the meaning of the same data, and don't share one canonical implementation, an attacker can sometimes make them disagree in their favor.

3. JuliaHub / General Registry: resolving versions through Git and an external API

Julia's package ecosystem resolves package versions in part by consulting the package's source Git repository directly, asking a Git operation to resolve a reference (a branch, tag, or commit) within a given subdirectory, and separately calling the GitHub API to look up repository metadata such as the canonical clone URL. Ng's research found that the values feeding into both of those operations, a repository reference string, and a repository URL sourced from an API response, were not fully constrained before being incorporated into command-line invocations of external tools (a Git subprocess) and into a URL later used to construct further requests.

This is the shape of a classic argument/command injection via insufficiently validated external input: the trusted party (the registry) faithfully consumes a value from a source it doesn't fully control (a repository's own metadata, or a field in a JSON API response) and passes it, with too little sanitization, into a context (a shell command, a URL construction routine) where unexpected characters change its meaning rather than its content. The specific fix path involved constraining which characters are permitted in these fields and using safer construction methods that don't allow argument or command boundaries to be manipulated by the data itself. The research also touched on a path-confusion angle, where a value expected to identify a single path segment could instead be shaped to traverse into unrelated resources, a variant of the "the same string means different things to different parsers" theme covered in Ghost Bits earlier in this series.

4. NuGet: which metadata file does the registry actually trust?

A NuGet package (a .nupkg file) is a ZIP archive that must contain exactly one .nuspec metadata file at its root, describing the package's ID, version, and dependencies. NuGet.org's backend processing pipeline performs an upload validation pass and then separate background jobs that traverse the archive to locate and use that .nuspec file for indexing, dependency resolution, and eventually generating the package's public registration metadata and searchable catalog entry.

The interesting finding is a selection-logic inconsistency between two different stages of that pipeline. The initial upload-validation stage and the later background-processing stage each independently implemented their own logic for "which file counts as the package's root .nuspec, and which files count as being in a subdirectory (and should therefore be ignored)." Ng showed that a specially-constructed archive could contain two different .nuspec files with two different declared package identities and dependency sets, arranged so that the validation stage's path-parsing logic considered one file to be "at the root" (and validated it as a normal, benign package), while the later processing stage's separate path-parsing logic selected the other file, the attacker's real, malicious metadata, when actually generating what gets published.

The result: a package that validates as benign, but publishes as something else entirely. Because the two pipeline stages disagreed about which file was "the" nuspec, an uploader could pass the safety check with one identity and dependency declaration, and have the registry publish a completely different one, including malicious dependencies, an SSRF-capable reference embedded in metadata fields the registry later fetches server-side (such as an icon URL), and ultimately a path where a crafted, dual-purpose file (a "polyglot" valid as both an image format and a ZIP archive) could be smuggled through an icon-processing step. Each stage was individually reasonable; disagreeing with its sibling stage is what broke the guarantee.

This is architecturally the cleanest example in the talk of the recurring lesson across this whole series (see also IntentGuard and the DFIR-agent boundary-perturbation talk): a multi-stage pipeline is only as consistent as its least-agreeing pair of stages, and "we validated it" means nothing if the thing that gets published afterward was selected by different logic than the thing that got checked.

5. CocoaPods: a probabilistic gap in publisher verification

CocoaPods (the package manager for Objective-C and Swift) verifies a new publisher's control of an email address via a time-limited verification code sent by email, a standard pattern used across countless registration systems. Ng's research examined the size of the code's value space and the length of its validity window together, rather than either property in isolation, and found that the combination made a birthday-paradox-style collision attack against the verification code space practical within the code's own validity period, given a realistic request rate. In other words, the code space wasn't small enough, or the validity window wasn't short enough, for the two together to resist a determined, moderately-resourced guessing campaign, a textbook illustration of why "add a random code" is not automatically sufficient without also reasoning quantitatively about the actual collision probability an attacker faces.

The practical consequence of defeating this verification step is registering as, or hijacking the registration flow for, a publisher identity you don't actually control, the entry point for a classic account/identity-based supply-chain compromise, just reached through a probability weakness in the verification mechanism rather than a credential leak.

6. The common thread across all four ecosystems

RegistryThe trust that broke
LuaRocksManifest/version data assumed to be structurally well-formed, and consistently interpreted across the value ranges different Lua runtimes actually support
JuliaHubRepository-sourced strings (a Git ref, an API-provided URL) assumed safe to pass into command and URL construction without full sanitization
NuGetTwo independent pipeline stages assumed to agree on "which file is the real metadata", they didn't
CocoaPodsA verification code's guessability assumed negligible without checking the math against its own validity window and achievable request rate

None of these is a novel bug class, injection via unsanitized external input, multi-stage validation disagreement, and undersized probabilistic secrets are all well-known categories individually. What makes this research notable is where they were found: not in some obscure internal tool, but in the backend of infrastructure that every developer in each of these four language ecosystems implicitly trusts every single day, often multiple times, without a second thought.

7. What this means for defenders

For registry operators:

  • Treat every stage of your ingest/publish pipeline as a potential trust boundary, and specifically test whether independently-implemented parsing/selection logic in different stages can be made to disagree about the same input.
  • Fully validate and constrain any externally-sourced string (a Git ref, an API response field, a URL) before it reaches a command-line invocation or a URL-construction routine; prefer parameterized/argument-array APIs over string concatenation into shell commands.
  • Quantify probabilistic security controls (verification codes, tokens, session identifiers) against their actual validity window and realistic attacker request rate, not just against a vague sense that the code space is "big enough."

For developers and organizations consuming these ecosystems:

  • "Trusted source" and "trustworthy" are not the same claim. Even a widely-used, well-maintained registry can have backend bugs; pin dependency versions and hashes, and use lockfiles that verify content integrity rather than trusting a name/version pair alone.
  • Where available, use registries' own signing and provenance features (package signing, Sigstore-style attestations) as a defense layer that doesn't depend on the registry's internal processing logic being bug-free.
  • Report registry-infrastructure anomalies, not just suspicious packages. This entire research direction exists because someone asked "what if the pipe itself is broken?" rather than only "what if this specific package is malicious?"
Are these vulnerabilities still exploitable?

No. The talk includes an explicit disclaimer that all the bugs discussed were reported and fixed. This is retrospective, defensive research explaining what was found and how it was mitigated, not a live-exploitation guide.

Isn't "don't trust user input" already a solved problem?

The principle is well known; applying it consistently across every stage of a large, evolved backend system, especially when two stages were written at different times by different people with different assumptions, is where real systems keep failing. That inconsistency between stages, not ignorance of the principle, is what these bugs exploited.

Does this mean I shouldn't trust package registries at all?

The researcher's own closing point is more measured: "even trusted sources are not necessarily fully trustworthy." The practical response isn't distrust, it's defense in depth: pin versions and hashes, use lockfiles, prefer registries with signing/provenance, and don't treat "it's on the official registry" as a complete security guarantee on its own.

Why does this matter if I only use one of these four ecosystems?

The specific bugs are ecosystem-specific, but the underlying patterns (unsanitized external strings reaching command/URL construction, multi-stage validation disagreement, undersized probabilistic secrets) apply to essentially any package registry, build system, or artifact pipeline. If you operate similar infrastructure for an internal package feed or a CI artifact store, the same questions are worth asking of your own system.

Takeaways

  • The registry itself is part of the supply-chain trust boundary, not just the packages it distributes; this research targeted registry backend logic directly across four ecosystems.
  • LuaRocks: version/manifest data interpreted inconsistently across processing contexts, including a cross-runtime numeric-precision gap.
  • JuliaHub: externally-sourced strings (Git refs, API-provided URLs) insufficiently sanitized before use in command and URL construction.
  • NuGet: two independent pipeline stages disagreed about which file in an archive was "the real" metadata, letting a package validate as benign while publishing as malicious.
  • CocoaPods: a verification code's size and validity window, combined, were vulnerable to a birthday-paradox-style collision attack.
  • All findings were responsibly disclosed and are already fixed; the durable lesson is architectural: audit consistency between pipeline stages, not just within any single stage.

References & further reading

← prev: SHAR DNS Attack next: Graph-Aware Logon Detection →
© cvam — written in plaintext, served warm