Skip to content

http.client aborts header parsing upon encountering non-ASCII header names #81274

Open

Description

@tipabu
mannequin
BPO 37093
Nosy @warsaw, @bitdancer, @maxking, @corona10, @tipabu
PRs
  • gh-81274: Allow http.client to parse non-ASCII header names #13788
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = None
    created_at = <Date 2019-05-29.23:32:09.269>
    labels = ['3.7', '3.8', 'type-bug', 'library', '3.9']
    title = 'http.client aborts header parsing upon encountering non-ASCII header names'
    updated_at = <Date 2022-01-22.03:59:08.693>
    user = 'https://github.com/tipabu'

    bugs.python.org fields:

    activity = <Date 2022-01-22.03:59:08.693>
    actor = 'corona10'
    assignee = 'none'
    closed = False
    closed_date = None
    closer = None
    components = ['Library (Lib)']
    creation = <Date 2019-05-29.23:32:09.269>
    creator = 'tburke'
    dependencies = []
    files = []
    hgrepos = []
    issue_num = 37093
    keywords = ['patch']
    message_count = 2.0
    messages = ['343942', '358833']
    nosy_count = 5.0
    nosy_names = ['barry', 'r.david.murray', 'maxking', 'corona10', 'tburke']
    pr_nums = ['13788']
    priority = 'normal'
    resolution = None
    stage = 'patch review'
    status = 'open'
    superseder = None
    type = 'behavior'
    url = 'https://bugs.python.org/issue37093'
    versions = ['Python 3.7', 'Python 3.8', 'Python 3.9']

    Activity

    1. tipabu commented on May 29, 2019

      tipabumannequin
      MannequinAuthor

      First, spin up a fairly trivial http server:

          import wsgiref.simple_server
          
          def app(environ, start_response):
              start_response('200 OK', [
                  ('Some-Canonical', 'headers'),
                  ('sOme-CRAzY', 'hEaDERs'),
                  ('Utf-8-Values', '\xe2\x9c\x94'),
                  ('s\xc3\xb6me-UT\xc6\x92-8', 'in the header name'),
                  ('some-other', 'random headers'),
              ])
              return [b'Hello, world!\n']
          
          if __name__ == '__main__':
              httpd = wsgiref.simple_server.make_server('', 8000, app)
              while True:
                  httpd.handle_request()

      Note that this code works equally well on py2 or py3; the interesting bytes on the wire are the same on either.

      Verify the expected response using an independent tool such as curl:

          $ curl -v http://localhost:8000
          *   Trying ::1...
          * TCP_NODELAY set
          * connect to ::1 port 8000 failed: Connection refused
          *   Trying 127.0.0.1...
          * TCP_NODELAY set
          * Connected to localhost (127.0.0.1) port 8000 (#0)
          > GET / HTTP/1.1
          > Host: localhost:8000
          > User-Agent: curl/7.64.0
          > Accept: */*
          > 
          * HTTP 1.0, assume close after body
          < HTTP/1.0 200 OK
          < Date: Wed, 29 May 2019 23:02:37 GMT
          < Server: WSGIServer/0.2 CPython/3.7.3
          < Some-Canonical: headers
          < sOme-CRAzY: hEaDERs
          < Utf-8-Values: ✔
          < söme-UTƒ-8: in the header name
          < some-other: random headers
          < Content-Length: 14
          < 
          Hello, world!
          * Closing connection 0

      Check that py2 includes all the same headers:

          $ python2 -c 'import pprint, urllib; resp = urllib.urlopen("http://localhost:8000"); pprint.pprint((dict(resp.info().items()), resp.read()))'
          ({'content-length': '14',
            'date': 'Wed, 29 May 2019 23:03:02 GMT',
            'server': 'WSGIServer/0.2 CPython/3.7.3',
            'some-canonical': 'headers',
            'some-crazy': 'hEaDERs',
            'some-other': 'random headers',
            's\xc3\xb6me-ut\xc6\x92-8': 'in the header name',
            'utf-8-values': '\xe2\x9c\x94'},
           'Hello, world!\n')

      But py3 *does not*:

          $ python3 -c 'import pprint, urllib.request; resp = urllib.request.urlopen("http://localhost:8000"); pprint.pprint((dict(resp.info().items()), resp.read()))'
          ({'Date': 'Wed, 29 May 2019 23:04:09 GMT',
            'Server': 'WSGIServer/0.2 CPython/3.7.3',
            'Some-Canonical': 'headers',
            'Utf-8-Values': 'â\x9c\x94',
            'sOme-CRAzY': 'hEaDERs'},
           b'Hello, world!\n')

      Instead, it is missing the first header that has a non-ASCII name as well as all subsequent headers (even if they are all-ASCII). Interestingly, the response body is intact.

      This is eventually traced back to email.feedparser's expectation that all headers conform to rfc822 and its assumption that anything that *doesn't* conform must be part of the body: https://github.com/python/cpython/blob/v3.7.3/Lib/email/feedparser.py#L228-L236

      However, http.client has *already* determined the boundary between headers and body in parse_headers, and sent everything that it thinks is headers to the parser: https://github.com/python/cpython/blob/v3.7.3/Lib/http/client.py#L193-L214

    2. added
      stdlibStandard Library Python modules in the Lib/ directory
      type-bugAn unexpected behavior, bug, or error
      on May 30, 2019
    3. tipabu commented on Dec 23, 2019

      tipabumannequin
      MannequinAuthor

      Note that because http.server uses http.client to parse headers [0], this can pose a request-smuggling vector depending on how you've designed your system. For example, you might have a storage system with a user-facing HTTP server that is in charge of

      • authenticating and authorizing users,
      • determining where data should be stored, and
      • proxying the user request to the backend

      and a separate (unauthenticated) HTTP server for actually storing that data. If the proxy and backend are running different versions of CPython (say, because you're trying to upgrade an existing py2 cluster to run on py3), they may disagree about where the request begins and ends -- potentially causing the backend to process multiple requests, only the first of which was authorized.

      See, for example, https://bugs.launchpad.net/swift/+bug/1840507

      For what it's worth, most http server libraries (that I tested; take it with a grain of salt) seem to implement their own header parsing. Eventlet was a notable exception [1].

      [0] https://github.com/python/cpython/blob/v3.8.0/Lib/http/server.py#L336-L337
      [1] eventlet/eventlet#574

    4. transferred this issue fromon Apr 10, 2022
    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

      3.7 (EOL)end of life3.8 (EOL)end of life3.9 (EOL)end of lifestdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

      Projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions