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. That means that your Ruby app’s communication with Amazon Web Services, your credit card processor, and any other HTTPS services you might use can be vulnerable to this kind of attack. In an era of shared hosting environments, it’s not hard to imagine the path to a successful attack via this vector.
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_uriis 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.
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.