Problem/Motivation
Drupal does not currently verify that SSL certificates use a trusted root cert. This affects any requests initiated with drupal_http_request. It also affects OpenID discovery and HTTPS redirects during that discovery.
Proposed resolution
This issue includes solutions for Drupal 8 and the Drupal 7 backport.
For Drupal 8
Given we use Guzzle now, utilize http://guzzle.readthedocs.org/en/latest/http-client/client.html#configur... to nominate the system certificate file as the default instead of that shipped with Guzzle. Anyone wanting to use their own can implement ServiceModifierInterface and nominate an absolute file path.
For Drupal 7
1) Verify SSL certificates using curl_verify_ssl_peer. This only works when PHP is compiled with cURL support.
One drawback to this approach is that cURL is not always available in shared host environments. Commenters indicated that cURL is a relatively standard feature, and this should not be as much of a factor when D8 is released.
2) Include in Drupal a bundle of X.509 certificates of public Certificate Authorities (CA) borrowed from the cURL library.
greggles mentioned in comment 24 that this might require rapid security advisories in the case of a compromised cert. Also, including certs in Drupal core would logically add a configuration requirement where admins could add new certs.
Remaining tasks
Get Drupal 8 patch in.
Backport to Drupal 7 - Comment #25 includes a patch using curl_verify_ssl_peer, but it requires testing.
Possibly backport to D6.
API changes
This could affect developers who use drupal_http_request to issue HTTPS requests against servers that do not have proper SSL certs. For example, HTTPS connections would fail when issued to an internal dev server that used a self-signed cert.
Original report by Heine
Wojtha just asked on IRC if certificates are verified. They are not.
Demo patch in a few mins.
Comment | File | Size | Author |
---|---|---|---|
#38 | 1081192.patch | 978 bytes | klausi |
#35 | ssl-peer-1081192.35.patch | 632 bytes | larowlan |
Comments
Comment #1
Heine CreditAttribution: Heine commentedHere's a quick initial patch.
Obviously needs testing.
But also, discussion:
- maybe a 'drop-in' cert store would be better?
- Should we use a central cert store and provide an API?
- Are the certs allowed under the GPL?
- (How) can we use the certificates that come with certain Unices?
Comment #2
wojtha CreditAttribution: wojtha commentedI was asking in relation to OpenID discovery and HTTPS redirects during that discovery.
I dived into the JanRain OpenID and there is nothing like this.But it makes sense to me to verify HTTPS connections against trusted roots and give possibility to users/site admins to extend these trusted roots - as every common internet browser does these days.I wasn't right with the JanRain, it uses curl to verify hosts - so it uses actually the same trusted roots as Heine included in his patch. But unfortunately JanRain library uses curl HTTPS verification in a bad way - see https://github.com/openid/php-openid/issues#issue/38.
And also this needs to be backported to Drupal 6 IMO.
Comment #3
Heine CreditAttribution: Heine commentedAs to the license:
Comment #4
c960657 CreditAttribution: c960657 commentedI think we should make this a feature of drupal_http_request() rather than something used specifically for OpenID.
I doubt many users will change this setting, so I think it is sufficient to point to one .crt file that is distributed with Drupal. The file name can be wrapped in a variable_get('ca_bundle', DOCUMENT_ROOT . 'misc/ca_bundle.crt') call so that people wishing to change the file can copy the original file into a different location and edit it, or possibly point to a central file on their system. In the latter case it would be relevant to support both cafile and capath.
Why is the .crt file called curl-ca-bundle.crt? It isn't specifically connected to curl, is it?
Comment #5
wojtha CreditAttribution: wojtha commented+1 for more general solution as c960657 pointed out. I will rather place it to drupal_http_request too.
@Heine: Will the general - core library level - solution cause some significant drawback?
@c960657: Heine took the bundle from the CURL library
Comment #6
wojtha CreditAttribution: wojtha commentedas to the API and UNICES:
I think we have two ways:
1) simple: allow user to override default CRT file with his own like c960657 mentioned in #4 #1081192-4: Verify peer on HTTPS if cURL available (but be careful of built-in cert bundles in the codebase)
2) more robust: provide API similar to drupal_add_css() which will allow modules to add their own certificates. For better performance, there could be cached collection of certificates in DB or in a file. But I guess it will be dangerous to store cached certificates in tmp folder with 777 permissions. (Or am I too paranoid?)... This will create place for more advanced work with certificates. I could imagine some contrib modules like e.g. Root Certificates UI for certificates administration...
Anyway both alternatives leads to implement this as default context inside drupal_http_request if no $context is specified and HTTPS scheme is detected IMO.
Comment #7
wojtha CreditAttribution: wojtha commentedTrying to move it to the drupal_http_request. I basically took Heine's patch, roll back his changes in openid and I moved ca-bundle.crt to the /misc directory.
Tested sucessfully:
HTTP:
HTTPS - untrusted root certificate:
HTTPS - trusted root certificate:
When I append untrusted certificate to the ca-bundle:
Comment #8
wojtha CreditAttribution: wojtha commentedUsing variable_get('ca_bundle') as suggested c960657 in #4 #1081192-4: Verify peer on HTTPS if cURL available (but be careful of built-in cert bundles in the codebase).
Following text added to the default.settings.php:
Comment #9
Heine CreditAttribution: Heine commentedThis issue was created before the 8.x-dev core branch was made and needed to limit API changes. This approach is better, but not suitable for 7.x IMO.
Moving to 8.x-dev for now.
Comment #10
wojtha CreditAttribution: wojtha commented@Heine: Ok, so "my approach" for 8.x dev and "your approach" for 7.x dev? But even in your case - openid only - we need some settings to allow override the default ca bundle IMO.
I don't think it is too big change, especially if it will be limited to openid module only - remember e.g. the semaphore introduced in 6.16.
Comment #11
c960657 CreditAttribution: c960657 commentedChanging drupal_http_request() to enforce strict SSL checking is probably too late for D7. But how about adding a new option to the option array, e.g. $option['secure_ssl'], as a short-hand for this blurb:
The new option should default to FALSE in D7 but possibly to TRUE in D8.
I think this is taking the explanation a bit too far. The settings.php file itself should be kept read-only, but we don't go into detail about how to do that.
Would it make sense to allow $conf['ca_bundle'] to be an absolute path? This would allow the usage of file in a central location, e.g. provided by the OS distribution.
Comment #12
wojtha CreditAttribution: wojtha commented@c960657 interesting thoughts
BTW interesting topic to this - detecting compromited certificates:
https://blog.torproject.org/blog/detecting-certificate-authority-comprom...
Comment #13
wojtha CreditAttribution: wojtha commentedPatch for D8, all three suggestions from #11 #1081192-11: Verify peer on HTTPS if cURL available (but be careful of built-in cert bundles in the codebase) included.
- ca-bundle path is now absolute
- introduced new option $option['secure_ssl'] which defaults to TRUE for D8, but it should defaults to FALSE for D7.
Comment #14
Dries CreditAttribution: Dries commentedGosh. I'd _really_ love to get rid of drupal_http_request() and rely on something provided by PHP. Should we make CURL a requirement?
Comment #15
wojtha CreditAttribution: wojtha commented@Dries I basically agree, but CURL isn't always available on shared hostings. Questions is do we really want to Drupal be hosted on such hostings without CURL? And will exist in two-three years horizont any reasonable PHP hosting without CURL support?
Comment #16
sunI don't think that removing dhr() would be a wise direction/decision. Not sure why you think we should get rid of it, @Dries.
cURL-over-PHP has its very own flaws, requiring deep understanding of a couple of things. dhr() itself contains some bits, HTTP request handling methods in DrupalWebTestCase contain some others. dhr() allows the average Drupal coder to easily do stuff without necessarily understanding all of the major things under the hood.
Comment #17
xjmTagging issues not yet using summary template.
Comment #18
catch#13: d8_certificates_api_1081192.patch queued for re-testing.
Comment #20
wojtha CreditAttribution: wojtha commentedReroll from #13 with just little cosmetic change. Returning to needs review status based on comment #16 by sun.
Comment #21
chx CreditAttribution: chx commentedAdding root certificates without an easy and quick way to revoke is a particularly unwise idea. cURL can handle with OS updates. We can't. Example:
So while cURL might be more complex I dont see another easy way out.
Comment #22
wojtha CreditAttribution: wojtha commented@chx Might be we can provide one built-in bundle which will be included automatically + give option to site admin add own CA file.
So if another "DigiNotar Root CA" cause happen we can release Drupal security update, user defined certificates will be unaffected. And also user defined certificates will be user's responsibility... the same responsibility as not-updated drupal site.
Anyway even if the one certificate from the bundle will be compromised, the provided solution will be still better than the current state, when Drupal totally don't care about https security.
Or would be better to import the certificates to the database table? I prefer the config file, but I'm just curious if someone finds some kind of admin UI useful to manage the certificates.
Comment #23
chx CreditAttribution: chx commentedTo clarify: the proposal is to add peer verify only if you have cURL. We still work w/o cURL just less secure.
Comment #24
gregglesIn general this idea feels tough for us to do. It increases the number of things we have to pay attention to and do an SA about. It's also the kind of thing where we would get the news *after* something bad happened so it would encourage us to do a release very quickly which is not great.
I think chx' proposal in #23 is a solid idea ;)
Comment #25
nedjoHere's a crack at chx's suggestion in #23, untested. PHP reference: http://php.net/manual/en/context.curl.php.
Comment #26
mikeytown2 CreditAttribution: mikeytown2 commentedMoving back to D7 as D8 uses Guzzle now.
Comment #27
David_Rothstein CreditAttribution: David_Rothstein commentedAs far as I can tell, Guzzle does this by default. However, it does so using a cacert.pem file that is shipped with the Guzzle codebase. So basically, this is the exact scenario @chx and @greggles were concerned about above?
Moving back to D8 for discussion of whether Drupal actually needs to override that...
Comment #28
mikeytown2 CreditAttribution: mikeytown2 commentedI believe the file is from mozilla so we could track it.
https://github.com/guzzle/guzzle/issues/280
https://forums.aws.amazon.com/message.jspa?messageID=400240
http://guzzlephp.org/tour/http.html#configuration-options
Using the system cert instead of the bundled one might be a way around this issue
'ssl.certificate_authority' => 'system',
Comment #29
gregglesGreat find, mikeytown2. I'd say setting it to system makes more sense than shipping our own.
Comment #29.0
gregglesUpdated issue summary as part of drupalofficehours.org, with advice from xjm.
Comment #30
martin107 CreditAttribution: martin107 commented25: verify-ssl-peer-1081192-25.patch queued for re-testing.
Comment #32
martin107 CreditAttribution: martin107 commentedComment #33
ahmed25 CreditAttribution: ahmed25 commentedComment #34
ahmed25 CreditAttribution: ahmed25 commentedThis does not need a reroll , it needs a re think because it has been replaced by guzzle.
Comment #35
larowlanI don't think we can ship with the bundled guzzle cacert.pem, so this makes the change suggested by @mikeytown2 #28 and as per http://guzzle.readthedocs.org/en/latest/http-client/client.html#configur...
Bumping to critical as security follow-ups/issues are always critical.
If someone wants to use their own, they can implement ServiceModifierInterface and alter the arguments to nominate an absolute path as per the documentation.
Comment #36
klausiYes, we should not use the Guzzle shipped cacert.pem. I guess there is no meaningful way we can automatically test this?
Is there an upstream issue/ticket to use the system certificate file per default?
Comment #37
tstoecklerAwesome patch!
Not pushing back to "needs work" but could we add a comment above that line with some of the considerations in this issue. I.e. if someone swaps out the http_client they might not be aware of the implications. For a security feature this is pretty hidden away.
Comment #38
klausiklausi opened a new pull request for this issue.
Comment #39
klausiLike this? Suggestions for better wording welcome.
Comment #40
tstoecklerYay! That's exactly the reason why I didn't propose an example description: I couldn't have described it this precisely.
Comment #41
gregglesThe comment looks good to me. It explains why this setting is made.
Comment #42
catchI opened https://github.com/guzzle/guzzle/issues/623 against guzzle.
Committed/pushed to 8.x, thanks! Moving to 7.x for backport.
Comment #44
selfuntitled CreditAttribution: selfuntitled commentedJust testing the D7 Backport. Looks like the patch doesn't apply because it was created with /core/ in the path.
If you drop /core/ it applies just fine. Haven't yet given it a bad cert to test.
Comment #45
DamienMcKenna@selfuntitled: What patch did you use for D7? I don't see one included here, and the one from #38 does not apply (there is no core.services.yml file).
Is this all that's needed? It turns on curl_verify_ssl_peer and ssl['verify_peer'] in the context when using https connections. FYI PHP 5.6 enables verify_peer and verify_peer_name by default.
Comment #46
DamienMcKennaThe $options['context'] has to be an actual resource object created by stream_context_create(), not just an array.
Comment #48
DamienMcKennaI have no idea why my patch in #46 would case FileNameMungingTest->testMunging() to fail?
Comment #50
DamienMcKennaOk, it worked the second time. Go figure.
Comment #51
gregglesThe latest patch no longer sets verify_peer to true in any cases. Is that your intent?
Comment #52
DamienMcKennaWhoops, sorry, copy/paste mixup.
Comment #53
selfuntitled CreditAttribution: selfuntitled as a volunteer commented@DamienMcKenna - I was working from the D7 patch by nedjo which failed testing in comment #25.
This was almost a year ago that I was playing with this, and my memory is that, for whatever reason, the patch didn't solve the problem and my client didn't want to pay for me to troubleshoot, so I just implemented what I needed directly with curl and didn't bother with drupal_http_request.
If I get some further time, I'd love to poke at the D7 backport of this, but haven't thought to look at it since.
Edit - just reviewing your other comments and patch, I'm happy to test with my use case from last year.
Comment #54
mcdruidHow about making the SSL context options configurable via a variable?
This would allow existing sites to opt-out of this change, and those moving to PHP 5.6 can be set up in a way that's backwards compatible with the way they used to work - for example being able to accept self signed certificates.
Attached is a patch which does the same thing as that from #52, but uses a variable to make the options configurable.
This makes it possible to do something like this in settings.php
$conf['drupal_ssl_context_options'] = array('verify_peer_name' => FALSE);
Comment #55
Heine CreditAttribution: Heine commentedThis is probably a requirement to fix #1538118: Update status does not verify the identity or authenticity of the release history URL for Drupal 7. Globally setting ssl context options would be undesirable, because it could make the update process vulnerable to mitm.
Comment #56
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedYeah, I don't think it should be overridable globally - just by the caller of drupal_http_request(). In the patch in #52 it is, although the method for doing so is awkward; however it is not a common need so it's probably OK if we just document it.
Questions:
Comment #57
hass CreditAttribution: hass commentedThis is not only an issue of incorrect certificates developers may use. Nearly all installations are affected except a few systems with updated ca list.
The root cause is cURL that was distributed too long with bad default ca list. My dev machine has xampp with php 5.6 and fails to request recaptcha webservice. As guzzle uses curl in most cases this means ssl peer verification causes fails on nearly every system. I have not disabled peer verification and documented this at Known issue with cURL and outdated root certificates.
I hope and support adding the latest ca to core to get this issues resolved by default.
Comment #58
sanduhrsIn PHP < 5.6 verify_peer default was FALSE, verify_peer_name didn't exist.
in PHP 5.6 verify_peer has been changed to default to TRUE, verify_peer_name has been introduced and the default set to TRUE.
So to enable certificate verification on an old system with PHP <5.6 we must set verify_peer to TRUE. For systems >=5.6 the defaults should be fine.
We could add a check to hook_requirements to inform people about their status.
Comment #59
sanduhrsWe should probably add a CN_match for PHP <5.6 which is the ancestor of peer_name in PHP >=5.6
See http://php.net/manual/en/context.ssl.php
Comment #60
pwolanin CreditAttribution: pwolanin as a volunteer commentedI'm not sure the summary reflects the discussion or patch for Drupal 7?
Comment #65
DamienMcKennaIt's been a year, maybe we can get this finished off?
Comment #66
Fabianx CreditAttribution: Fabianx as a volunteer commentedPer the new policies lets open up a follow-up for this issue for D7 with title:
"[D7] Verify peer on HTTPS if cURL available (but be careful of built-in cert bundles in the codebase)"
Comment #67
Elijah LynnThe option to set these with a variable in #54 works well. In our case we need to actually set the "peer_name" in an AWS ELB (DEV with no CNAME) with Advagg doing some checks on /reports/status and this solves that use case as well.
I don't like the idea of turning "verify_peer" on by default for <5.6. I think with the release of this patch we should recommend adding that to settings.php and add an example there as well.
Comment #68
kmcculloch CreditAttribution: kmcculloch commentedHere is an approach that allows for global defaults as well as per-host overrides. It requires a slightly more complicated configuration array in settings.php:
This patch does not apply any new defaults for PHP<5.6 unless they're declared in settings.php. I think that's the wiser course, backwards-compatibility-wise, but I could be convinced otherwise.
Comment #70
DamienMcKennaI like the idea of #68 because usually you'll want to relax the security restrictions for a specific host rather than system-wide.
Comment #71
kmcculloch CreditAttribution: kmcculloch commentedForgot to supply default empty array for variable_get
Comment #72
david-urban CreditAttribution: david-urban as a volunteer commentedI have just tested #71 patch on my https lets encrypt site and got my favourite "The NodeSquirrel server returned the following error:". I would say the patch does not work on php 7.1.
Comment #73
mcdruid+1 for the approach in #71 allowing overrides per host.
I outlined something similar in #2774269: provide conditional configuration.
Comment #74
hitfactory CreditAttribution: hitfactory commentedConfirming patch in #71 solves my issue #2864550: How to set stream_context in SearchApiSolrConnection. Thanks, @kmcculloch.
Comment #75
stefan.r CreditAttribution: stefan.r commentedComment #76
brice_gato CreditAttribution: brice_gato commentedA quick workaround :
// You can override drupal_ssl_context_options through settings.php like comment#68
Comment #77
coolestdude1 CreditAttribution: coolestdude1 as a volunteer commented+1 for the patch in #71
Setup: PHP7.1, Apache 2.4, MariaDB 10
Detailed:
mariadb101u.x86_64 1:10.1.29-1.ius.centos7 @ius
mod_php71u.x86_64 7.1.11-1.ius.centos7 @ius
httpd.x86_64 2.4.6-67.el7.centos.6 @updates
So yet another RTBC
Could we document a little better the 'my-self-signed-solr-test-server@example.com' part of the patch?
If the check from the patch is in_array($uri['host'], array_keys($opts)) would we want to say that 'base_url' or the value inside of base_url is an acceptable option, particularly for self requests?
Comment #79
MustangGB CreditAttribution: MustangGB commentedLooks like this needs a reroll.
Comment #80
kmcculloch CreditAttribution: kmcculloch commentedHere's a re-roll.
I added a usage example to default.settings.php and changed the example host entry to 'my-self-signed-solr-test-server.example.com' to address @coolestdude1's concerns. (Not sure why there was an "@" sign in the example host name, but I agree that's pretty confusing.)
Comment #81
Fabianx CreditAttribution: Fabianx as a volunteer commentedI am still very much +1 to the patch and would RTBC it, but please create a new issue for it, because the 8.x issue code change is different than the intention here.
#2761345: PHP 5.6, 7 - drupal_http_request() behavior changes for SSL using self-signed certs
might be a good candidate and we might also want to set a default 'default' entry of securing even older PHP versions and deliberately break compatibility to make upgrade to newer versions of PHP easier (break early with docs, rather than it breaking when upgrading PHP).
Thanks!
Comment #82
joseph.olstadBumping to 7.61. This didn't make it into 7.60.
Comment #83
joseph.olstadsee the new issue as per FabianX's request in comment #81
I think this issue can be closed, continue on in .
#3017522: Make SSL options configurable in drupal_http_request()
Comment #84
joseph.olstadActually this has to get into D8 first, as per comment #25
latest D8 patch probably needs a reroll too.
Comment #85
izmeez CreditAttribution: izmeez commentedIt looks like it was already committed to D8, see comments #42, and #60-64. Thanks.
Comment #86
poker10 CreditAttribution: poker10 at ActivIT s.r.o. commentedAs per @izmeez, this issue was committed to D8 in #42, so changing the status to Fixed, so that the D8 patch contributors can get credits. In the meantime, D7 "backport" is discussed here: #3017522: Make SSL options configurable in drupal_http_request() , though it is a bit different approach now.