Skip to content

SSL validation should be enabled by default #255

Closed

Description

@florimondmanca

Right now our HTTP connection pool and connection implementations do this kind of thing:

self._ssl_context = SSLContext() if ssl_context is None else ssl_context

i.e. when ssl_context is None (not passed), then we default to an empty SSLContext.

This dates back from the initial implementation from #11.

Is this the right thing to do? Well, maybe not.

TL;DR

I think we should default to not instantiating any ssl_context by default, and let that responsibility to the caller code. "Do one thing and do it well", but not only — see below.

Reproduction case

Say you have a local HTTPS server that's using custom certs, eg ones generated via trustme-cli.

from starlette.responses import PlainTextResponse

app = PlainTextResponse("Hello, World!")
trustme-cli
uvicorn --ssl-keyfile server.key --ssl-certfile server.pem app:app

In this situation, we could expect clients to have to pass their client.pem certificate to the server. All other cases should fail by default, right?

But… right now it's actually possible to request the HTTPS server using HTTPCore without passing an ssl_context — i.e. through the default configuration.

For example, this succeeds:

import httpcore

with httpcore.SyncConnectionPool() as http:
    method = b"GET"
    url = (b"https", b"localhost", 8000, b"/")
    headers = [(b"host", b"localhost")]
    _, _, stream, _ = http.request(method, url, headers)
    try:
        body = b"".join([chunk for chunk in stream])
        print(body)
    finally:
        stream.close()
$ python client.py
Hello, world!

Why? 🤔

Our default is to use an empty SSLContext(). Contexts like these are actually instantiated in CERT_NONE mode:

>>> import ssl
>>> ssl.SSLContext().verify_mode
<VerifyMode.CERT_NONE: 0>

And so the certs don't get validated — in short, no SSL verification happens.

But if we pass an explicit context created via ssl.create_default_context(), which returns a context in CERT_REQUIRED mode… Then we get an expected failure.

import httpcore
import ssl

ssl_context = ssl.create_default_context()

with httpcore.SyncConnectionPool(ssl_context=ssl_context) as http:
    method = b"GET"
    url = (b"https", b"localhost", 8000, b"/")
    headers = [(b"host", b"localhost")]
    # Oops...
    _ = http.request(method, url, headers)
httpcore.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1122)

If we add

ssl_context.load_verify_locations("client.pem")

then things get back to normal — SSL verification succeeds, and we get Hello, world!.

Additional context

I bumped into this while investigating encode/httpx#1424 and #254. Right now our tests rely on the fact that we're actually running in CERT_NONE mode — bypassing any SSL verifications with our test servers. But for HTTPS proxy support I'm actually encountering complications related to cert verification that we're not seeing in the rest of our tests because of this.

Activity

  1. florimondmanca commented on Dec 15, 2020

    @florimondmanca
    ContributorAuthor

    There's also the option of actually keeping this "no-verify by default" behavior, but updating the test suite so we properly pass certs whenever we ought to.

    I'm not sure if that's the most fail-safe option though. We could very well mess things up in the future again and not know about it… And that's a big smell for the behavior we should be exposing to users as well.

  2. florimondmanca commented on Dec 15, 2020

    @florimondmanca
    ContributorAuthor

    There's also the option of just changing the ssl.SSLContext() default to ssl.create_default_context(). To the user that doesn't change anything I think — SSL validation will fail in the same places and in the same ways. But internally this might result in bit cleaner code.

  3. changed the title [-]`ssl_context=None` should not result in using no-verify `SSLContext()`[/-] [+]SSL validation should be enabled by default[/+] on Dec 15, 2020
  4. lovelydinosaur commented on Nov 16, 2021

    @lovelydinosaur
    Member

    On by default in 0.14 👍

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingrefactor

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions