Emerging Technology

Reflection-Based Dependency Confusion Techniques

Dependency confusion is moving beyond name-typosquat. Reflection-based techniques let attackers hijack packages through dynamic imports and runtime resolution.

Shadab Khan
Security Engineer
8 min read

Alex Birsan's 2021 disclosure of dependency confusion fundamentally changed how enterprises thought about internal package namespaces. The attack was elegant: publish a package to a public registry with the same name as an internal private package, and package managers whose version-resolution logic preferred the higher semver version would silently pull the public one. Birsan's original report described successful executions against Apple, Microsoft, PayPal, Shopify, Netflix, Yelp, Uber, Tesla, and dozens of other Fortune 500 targets.

Five years later, the naive form of the attack is largely foreclosed by scoped namespaces, registry-configuration hygiene, and policy gates. What has grown in its place is a class of attacks I'll call reflection-based dependency confusion: techniques that exploit dynamic import mechanisms, reflection APIs, and runtime resolution paths to inject malicious packages even into projects that have closed the classic Birsan vector. This post reviews the technical primitives, the public research, and the defenses that interrupt the newer patterns.

What Does Modern Dependency Confusion Look Like?

Classic dependency confusion attacks the manifest-resolution step: the package manager reads the package name, looks up all registries in scope, and picks a version according to semver rules. The fix is to ensure the private registry is authoritative for the private scope, either via scoped package names (@company/internal) or via registry-configuration that blocks public lookup for specific namespaces.

Reflection-based attacks move one layer down. They target code paths that resolve a package name at runtime, typically via dynamic import, reflection, or plugin-discovery mechanisms. The package manager's install-time resolution may have correctly avoided the public confused package. The runtime resolution, which operates against a different search path and different trust boundary, may not.

The 2025 research cluster on this pattern includes work by Socket Security, Phylum, and independent researcher Anil Karaka. Their findings emphasize that plugin-style architectures, dynamic require() in Node.js, Python's importlib and entry_points, Ruby's Bundler.require with groups, and Java's ServiceLoader all present reflection-based resolution surfaces that dependency-resolution policy does not fully cover.

Which Specific Techniques Show Up in the Research?

Several specific techniques appear across multiple 2025 disclosures. First, plugin-directory poisoning: an application uses fs.readdir or equivalent to enumerate a plugins directory and require each one. An attacker who can write to that directory drops a plugin with a filename that sorts before the legitimate plugins, and hijacks initialization. This is not strictly dependency confusion; it is plugin-loading poisoning, but the pattern of "runtime enumeration of loadable code" is shared.

Second, entry-points hijack: Python's setuptools entry_points mechanism lets packages register plugins under a well-known group name. An attacker who can publish a package that registers for the same entry point, and whose package is installed in the environment, gets their plugin loaded by any consumer that iterates entry_points. Socket documented multiple 2024 and 2025 PyPI packages using this technique against widely-used entry-point groups like "console_scripts" and framework-specific groups.

Third, dynamic-require with user-controlled input: the application constructs a require() or import() call using a string derived from configuration or environment. A configuration file that admits an arbitrary package name redirects the runtime resolver toward an attacker-supplied name. If the resolver falls back to public registry fetch, which some in-process resolvers do in development modes, the attack becomes a one-step confused-scope compromise.

Fourth, name-collision inside workspaces: monorepo workspaces with multiple package.json files can introduce a package of the same name as an internal dependency, with the local workspace resolver preferring the nearest one. An attacker with write access to any workspace package can shadow an internal name for consumers elsewhere in the monorepo.

How Does This Differ from Typosquatting and Namespace Confusion?

Typosquatting targets human error at the manifest level. Namespace confusion targets resolver policy at the install level. Reflection-based confusion targets resolver policy at the runtime level. The distinction matters because the defenses are different. Typosquatting is caught by lockfiles and automated name-validation tools. Namespace confusion is caught by scoped packages and registry configuration. Reflection-based confusion is caught by reachability analysis of the actual runtime resolver behavior, which most static SCA tools do not perform.

The research cluster emphasizes that many organizations have deployed strong install-time policies against dependency confusion and treated the problem as solved, without appreciating that their runtime plugin systems and dynamic-import code paths present a different resolver surface. CISA's 2024 supply-chain advisory on plugin ecosystems called out this gap specifically.

What Is the Public Incident Record?

Confirmed public incidents in the reflection-based class are still relatively few, because the attacks require more setup than classic dependency confusion and the attribution is more difficult. Notable cases include the March 2024 PyTorch nightly incident, in which a malicious torchtriton package was published to PyPI and preferred over the PyTorch-official torchtriton package by pip's default resolver, specifically because the PyPI version was higher. This was a classic dependency confusion but hit because PyTorch's nightly build pulled from public PyPI. The October 2024 Ultralytics YOLO compromise, where a compromised build system published a malicious version of the ultralytics package, affected consumers that auto-updated. And the 2025 disclosures of Python entry-points hijacks against specific framework ecosystems including Django, Flask, and FastAPI plugin groups.

More broadly, the Socket blog, Phylum's research feed, and Endor Labs' supply-chain research reports through 2024 and 2025 document dozens of individual packages that implement one or another reflection-based technique, with varying degrees of confirmed successful exploitation. The shape of the pattern is validated; the absolute count of publicly attributed major breaches is still catching up.

What About Java, Go, and Rust Ecosystems?

The Java ServiceLoader mechanism is the classic reflection-based plugin loader in the JVM ecosystem. Any JAR on the classpath that provides a META-INF/services entry for a given service interface will be loaded by a consumer iterating via ServiceLoader.load(). This creates a natural vector for malicious code to register as a service implementation. 2024 research by the Oracle security team on JDK hardening discussed several CVEs in this shape, though they were typically in third-party libraries rather than the JDK itself.

Go's plugin package, rarely used in production due to its restrictions, has had its own share of supply-chain research. Rust's inventory and linkme crates, which provide compile-time plugin registration, are less exposed because registration happens at build time rather than runtime, but they have produced some reachability-relevant disclosures.

The cross-language takeaway: wherever the runtime iterates installed modules and loads them by convention, dependency confusion has a variant.

Which Defenses Actually Interrupt the Attack?

Defenses that work in practice: explicit allow-lists for plugin groups and entry points, so that the application loads only plugins on a known list rather than enumerating anything installed. Read-only plugin directories with cryptographic verification, for file-system-based plugin loaders. Reachability analysis that identifies dynamic import and plugin-load code paths and flags them for additional review, rather than treating the application as a pure static dependency graph. Scoped workspace names in monorepos to prevent cross-workspace shadowing. Explicit resolver configuration that forbids public fetch at runtime for specific package scopes, not just at install time. Sandboxing or capability-scoped execution for plugin code, so that a malicious plugin cannot reach sensitive credentials or outbound network without explicit permission.

At the pipeline layer, any build that produces an artifact containing a plugin-loader should have its plugin directory contents captured in provenance and reproducibly compared between builds. A drift in the set of plugins installed between two runs of the same build should be treated as a security event, not as environmental noise.

How Do You Find This in Your Own Code Base?

The practical question for a defender in 2026 is: where does my own code base do runtime reflection-based dependency resolution? Static analyzers can flag dynamic import(), require() with non-string-literal arguments, ServiceLoader.load() calls, importlib.import_module() with non-constant names, and similar patterns. Most projects have more of these than they realize, because frameworks embed them. Django's app registry, Rails' autoloader, Spring's component scan, and similar framework-level reflection systems are often the dominant source.

The practical recommendation is not to remove framework reflection — that's usually infeasible — but to inventory it, understand which namespaces it can load from, and ensure those namespaces are under the same controls as your primary dependency manifest.

How Safeguard.sh Helps

Reflection-based dependency confusion is fundamentally a reachability problem. Your static dependency graph does not show the plugin-loader code path that reaches into a runtime-resolved package name. Safeguard.sh's reachability analysis goes 100 levels deep and specifically identifies dynamic-resolution sites in your code base, so the runtime-loadable attack surface is visible alongside the install-time graph. Griffin AI prioritizes reflection-based advisories by whether your application actually invokes the relevant plugin loader, and triages the long tail of entry-point publications against your specific installed set.

Eagle watches for new disclosures in this attack class and enforces guardrails like plugin allow-listing and scoped resolver configuration before a poisoned plugin can be picked up. Continuous scanning re-evaluates runtime-resolvable surfaces on every build, catching drift when a new transitive dependency starts registering entry points under a group you consume. Container self-healing refreshes images when a runtime-resolvable dependency's provenance is called into question. For teams whose install-time policies are airtight but whose runtime resolvers are still doing real work, Safeguard.sh closes the second layer of the dependency-confusion problem.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.