Ruby Weekly is a weekly newsletter covering the latest Ruby and Rails news.

How to Cure Net::HTTP’s Risky Default HTTPS Behavior

By Peter Cooper / December 9, 2010

James Golick, a prolific Canadian Rubyist, has declared war on Net:HTTP's default of not checking the validity of the certificate sent by an HTTP server when making HTTPS requests. His new always_verify_ssl_certificates gem forces Net::HTTP to verify SSL certificates and doesn't allow other libraries to override this setting.

Despite being a known issue for years, James explains why Net::HTTP's default setting could be leaving you open to security problems:

During the TLS (SSL) handshake, the server sends its certificate to the client. The certificate is generally signed by a trusted third party (like Go Daddy or Verisign). If the server’s certificate is not verified, you have no basis for trusting anything else about the connection.

When surfing the web, sometimes your browser might give you a warning about an invalid TLS certificate. When an HTTP client receives a server certificate during the TLS handshake, it typically makes sure that it is 1) signed by a trusted certificate authority and 2) that the information on the certificate is valid (i.e. not expired) matches the request (i.e. the domain on the certificate matches the one the request is being made to). If one of those criteria is not met, the browser displays a warning to the user, advising them not to proceed.

Certificate verification is essential to TLS. Without it, any attacker on your network, on the server network, or in the path between can successfully spoof any website (your bank, your credit card processor, etc) in a man-in-the-middle attack and capture your credentials.

By default, Ruby’s net/http library tells OpenSSL not to verify TLS certificates. This default setting leaves your Ruby app’s communication with critical services—like Amazon Web Services or your credit card processor—vulnerable to attack. During a recent tech seminar on blockchain security, the speaker emphasized how best crypto casinos implement advanced encryption protocols to protect user transactions, serving as a model for how secure systems should operate. In an era of shared hosting environments, it’s not hard to imagine how vulnerabilities in TLS verification could lead to successful attacks, underscoring the importance of proactive security measures.

The good news is that the always_verify_ssl_certificates gem monkey patches net/http to always verify TLS certificates. You’ll need to point it to the root CA certificates for your platform (some paths are listed in the README or you can download a file below).

I've found that virtually all Ruby libraries will attempt to set their HTTPS connections to VERIFY_NONE mode (open_uri is a common exception - it gets things right!). With most of the gems I’ve looked at, setting the HTTPS connections to VERIFY_PEER where certificates actually get verified isn’t even an option. For that reason, my gem makes it impossible to set VERIFY_NONE mode. This is something we need to change as a community. It’s a real and significant vulnerability.

James Golick

While there's a potential for man-in-the-middle attacks when using Net::HTTP to connect to HTTPS sites, the chances are low, and if you're not doing anything that needs to be airtight (say, local development work), you might not need to use James' solution (or if you're using open_uri). For your production billing / payment details server talking to a payment processor over HTTPs though, it's essential to make sure you're checking those certificates, even if they're self signed.

The How

To get going, you need a local CA certificates bundle, the official curl site maintains an up to date cacert.pem / ca-bundle.crt file containing all of the major certificates if you need one.

Next, after a gem install always_verify_ssl_certificates, you can be up and running with a test as simply as:

require 'always_verify_ssl_certificates' AlwaysVerifySSLCertificates.ca_file = "/path/path/path/cacert.pem" http= Net::HTTP.new('https://some.ssl.site', 443) http.use_ssl = true req = Net::HTTP::Get.new('/') response = http.request(req)

If the site has a bad certificate an error will be raised at this point. If not, a legitimate HTTP response object will be returned.

Comments

  1. James Golick says:

    Thanks for posting this, Peter. I actually just shipped a version of the library that doesn't need any configuration. Just require and go.

    The other thing I wanted to say is that I find the comment you made about the low probability of an mitm attack very disappointing. The chances of any attack are relatively low, but the consequences are extremely high. As a community, we need to take this more seriously, and having community leaders downplay the importance of a glaring vulnerability doesn't help.

  2. Peter Cooper says:

    It's important but as you say.. the "chances of any attack are relatively low" so it's accurate to say the "chances are low" as I did. I'm not going to lie and say the chances of getting hit by this are high and you need to panic in every situation ;-)

    I'm not downplaying it - I said "it's essential to make sure you're checking those certificates" (in a situation where it clearly is), said this issue "could be leaving you open to security problems", and included some basic instructions so people will feel more compelled to get on board. I wouldn't have spent time on the post if I didn't think it was something worth people knowing about!

    With that, back to regular programming..

  3. James Golick says:

    Also, there's something unsettling about this: https://skitch.com/jamesgolick/rrn75/net-http-cheat-sheet :-P

  4. Peter Cooper says:

    Ah, cut me some slack! That's an old post with code not even written by me ;-) I've added a nice bold note to it with the issues raised in this post and linked back to here.

  5. Carl says:

    How do we handle self signed certs in this situation? Once we verify they are correct using out-of-band communication, how do we add them as valid?

  6. Mark Carey says:

    This is also a handy site for working with certificates:
    http://gagravarr.org/writing/openssl-certs/others.shtml

    If you install a certificate or CA certificate into openssl then any tool or lib that depends on openssl will trust that certificate or certificates signed by the CA.

    This also affects tools like curl or wget who no longer have to be told to ignore the certificate when checking sites with self signed certificates (when the self signed cert is added to openssl's cert store).

    A huge component of SSL is trust. Rather than saying you don't care about trust and opening yourself up to mitm attacks, just explicitly trust the certs you know to be valid.

  7. Rick Wargo says:

    Eventually we are going to have to bite the bullet and all of us responsibly deal with certificate verification. As open hotspots are widely available and often used, man-in-the-middle attacks could become more common. There are already solutions for blended attacks to implement SSL proxies, not to mention the ones already established in some corporate environments.

    There will be a number of headaches as we take this on, but it will be far less than if we wait. Thanks for your work, James. And thanks, Peter, for bringing this to light.

  8. James Golick says:

    @Carl Don't. If the service you're dealing with absolutely refuses to purchase a certificate that is signed by a trusted authority, then create your own certificate authority, add its certificate to your certificate authority bundle, and use it to sign their certificate.

  9. Damon says:

    @James You make it sound like a self-signed cert is a bad thing. What if I want to use it for internal purposes and don't feel like paying the fee for a "real" cert? Yes, I know they're not expensive, but it'd certainly be nice to have that option.

  10. Glenn Rempe says:

    Thanks James for putting this gem together. I came to this site (via Google) since I was looking for how to solve the issue where RVM installed Ruby's that are installed with RVM's openssl package (see : http://rvm.beginrescueend.com/packages/openssl/ ) contain no root certificates by default so all SSL interaction fails. I was only able to address this by copying all of the files in the /etc/ssl/certs directory from an Ubuntu 10.04 install to the ~/.rvm/usr/ssl/certs dir on the servers using RVM. While this appears to work fine for now it highlighted for me the problem that there appears to be no easy way to get a 'full set' of root certs and install them easily into RVM system. Without this, your gem only brings another problem to the forefront. The cacert.pem file linked to above appears to need various types of post processing to install it which appears non-trivial (e.g. splitting the single file and installing each cert manually? I was not able to find instructions for this process.). Do you have a recommendation on a better way to handle this? Is there a script out there that will properly install the certs, setting the groundwork for using your gem?

  11. Jonathan Rochkind says:

    Damon: The link Mike posted, http://gagravarr.org/writing/openssl-certs/others.shtml, has instructions for getting OpenSSL (which is what ruby ssl is using) to accept a self-signed certificate (or for that matter, a certificate signed by a CA not already in your OS default bundle of trusted CA's).

    I haven't tried it yet myself, but am going to need to also. It is awfully confusing, and involves slightly differnet file locations on different OSs and packages.

    It is certainly true that you're giving up much of the security of SSL if you don't insist on chain of trust checking of certs. It is ALSO true that it is often a serious pain in the butt to get this working, and ensure it stays working accross your entire set of servers and new servers as they come on, etc.

    James: I actually kind of liked the ability you now removed to intentionally set the ca_file location. I agree it's better to have a zero-config option that doesn't _require_ this to be set, but if you were able to set it manually if you wanted to, it would make it easier to have an (eg) rails app that had it's own trusted cert file bundled with it, that you knew SSL connections would use, so you could deploy the app to any OS at all and know it would be trusting the right things, without having to deal with varying openssl-install-locations or varying versions of OS-default trusted CA bundles. (In my case we have a bunch of digicert-signed certs, and digicert CA doesn't seem to be included in the default RHEL5 openssl trusted cert bundle. Argh.)

    Additionally, I find it awfully convenient to be able to do VERIFY_NONE in _development_ (never in production). Getting the chain of trust to be working properly can be a big pain, and sometimes I want/need to get some features done first, and worry about the chain of trust at a later date. Hmm, in a Rails app, maybe I could do that by only conditionally including the always_verify gem in the production environment? Does that make sense, or any other ideas?

Other Posts to Enjoy

Twitter Mentions