My first report on HackerOne: A logic flaw in npm

I submitted my first-ever report on HackerOne after discovering a logic flaw in www.npmjs.com’s URL routing fallback handler. The report was triaged and later accepted by GitHub’s bug bounty programme.

The Finding

While reading the npm documentation, I discovered a logic flaw on www.npmjs.com that allowed an adversary to plant malicious packages by polluting the namespace of archived pages. The www.npmjs.com application was configured in such a way that https://www.npmjs.com/<package-name> or https://npmjs.com/<package-name> redirected to the respective package on the registry under https://www.npmjs.com/package/<package-name>. The problem with this behaviour was the top-level directory also included pages belonging to npm. In other words, not packages on the registry, but actual website pages (e.g. https://www.npmjs.com/about).

This was exploitable whenever npm archived or deleted a page from the top-level directory. An example of this exploitable behaviour was discovered on the front page of npm’s documentation: https://docs.npmjs.com/about-npm (emphasis mine).

“To learn more about npm as a product, upcoming new features, and interesting uses of npm, sign up for our newsletter at npm-weekly, and be sure to follow @npmjs on Twitter.”

— Snippet from npm documentation

The npm-weekly link pointed to https://www.npmjs.com/npm-weekly which had since been deleted. This resulted in https://www.npmjs.com/npm-weekly redirecting to https://www.npmjs.com/package/npm-weekly—an unclaimed package. I was able to upload a package to the npm registry under the npm-weekly namespace which resulted in the npm documentation pointing to a “malicious” placeholder package under my control. An unsuspecting user may have inferred they had to install the npm-weekly package according to the documentation.

❯ npm publish
npm notice
npm notice 📦  [email protected]
npm notice === Tarball Contents ===
npm notice 5B   README.md
npm notice 169B package.json
npm notice === Tarball Details ===
npm notice name:          npm-weekly
npm notice version:       3.9.9
npm notice filename:      npm-weekly-3.9.9.tgz
npm notice package size:  253 B
npm notice unpacked size: 174 B
npm notice shasum:        2378ae60246ca96b6d78f8db5795958104c6276b
npm notice integrity:     sha512-QdDg5uXIv5Fej[...]o6/jWFgy2fCoQ==
npm notice total files:   2
npm notice
npm notice Publishing to https://registry.npmjs.org/
+ [email protected]

The Solution

I reported the behaviour to GitHub’s bug bounty programme on HackerOne and recommended npm create a routes list consisting of all current pages on www.npmjs.com. This would prevent redirects from ever occurring if the top-level path matched with one of the routes in the list. Then, the application could present the user with npm’s generic 404 page instead.

Based on an outdated version of npm’s source code still served on the npm registry under newww, it appeared that npm handled the npm-weekly redirection by passing the path to the fallback handler (handlers/fallback.js). The fallback handler was responsible for the redirect that resulted in the exploitable behaviour outlined in this write-up.

if (!validatePackageName(route).validForOldPackages) {
  return reply.view("errors/not-found", opts).code(404);
}

return reply.redirect("/package/" + route).code(302);

Using the newww codebase, I suggested a good alternative would be to ensure that the path passed from routes/public.js to handlers/fallback.js did not match any of the previous top-level paths. Only top-level routes needed to be considered here since the validatePackageName() would cause routes containing a / character to return a generic 404 page—package names cannot contain a /.

The following pseudocode-ish JavaScript roughly illustrates how this could have been done in the fallback handler.

var routes = [
  "private-npm",
  "org",
  "contact",
  "send-contact",
  "support",
  "whoshiring",
  // ...
  "login",
  "logout",
  "ping",
  "status",
  "csplog",
];

if (!validatePackageName(route).validForOldPackages || routes.includes(route)) {
  return reply.view("errors/not-found", opts).code(404);
}

return reply.redirect("/package/" + route).code(302);

Conclusion

This bug lead me to better understand URL routing and the importance of reviewing outdated source code for target applications. In cases like npm, the codebase has since transitioned to being closed-sourced. Had the newww source code not been available on the npm registry, another method would have been to search for forked repositories. Deleting a repository does not remove any previously forked repositories on GitHub.

The GitHub Security Team do not disclose low-severity bugs, but gave me permission to share my findings in this blog post. The reference to npm-weekly has since been removed from the official npm documentation in 710924e (#64).

I would like to thank the GitHub Security Team for their fast response time and professional triaging process. The team decided to award me a bounty and some GitHub merchandise despite the bug not being a high-impact issue. Their actions have motivated me to invest more time in searching for security flaws on GitHub-owned assets.


About me

I am a 2nd year BSc Cyber Security student at the University of Warwick working on security research. My areas of interest are web-application security, OSINT, and supply-chain attacks.


Contact me