Skip to content

Commit

Permalink
parse auth headers using pyparsing instead of regexp
Browse files Browse the repository at this point in the history
Fixes CPU burn DoS by cubic complexity of whitespace matching in
WWW_AUTH_RELAXED (default) regexp.
  • Loading branch information
temoto committed Jan 9, 2021
1 parent 595e248 commit b9a3c22
Show file tree
Hide file tree
Showing 11 changed files with 575 additions and 1,028 deletions.
493 changes: 107 additions & 386 deletions python2/httplib2/__init__.py

Large diffs are not rendered by default.

61 changes: 61 additions & 0 deletions python2/httplib2/auth.py
@@ -0,0 +1,61 @@
import base64
import re

import pyparsing as pp

from .error import *

UNQUOTE_PAIRS = re.compile(r"\\(.)")
unquote = lambda s, l, t: UNQUOTE_PAIRS.sub(r"\1", t[0][1:-1])

# https://tools.ietf.org/html/rfc7235#section-1.2
# https://tools.ietf.org/html/rfc7235#appendix-B
tchar = "!#$%&'*+-.^_`|~" + pp.nums + pp.alphas
token = pp.Word(tchar).setName("token")
token68 = pp.Combine(pp.Word("-._~+/" + pp.nums + pp.alphas) + pp.ZeroOrMore("=")).setName("token68")

quoted_string = pp.dblQuotedString.copy().setName("quoted-string").setParseAction(unquote)
auth_param_name = token.copy().setName("auth-param-name").addParseAction(pp.downcaseTokens)
auth_param = auth_param_name + pp.Suppress("=") + (token ^ quoted_string)
params = pp.Dict(pp.delimitedList(pp.Group(auth_param)))

scheme = token("scheme")
challenge = scheme + (token68("token") ^ params("params"))

authentication_info = params.copy()
www_authenticate = pp.delimitedList(pp.Group(challenge))


def _parse_authentication_info(headers, headername="authentication-info"):
"""https://tools.ietf.org/html/rfc7615
"""
header = headers.get(headername, "").strip()
if not header:
return {}
try:
parsed = authentication_info.parseString(header)
except pp.ParseException as ex:
# print(ex.explain(ex))
raise MalformedHeader(headername)

return parsed.asDict()


def _parse_www_authenticate(headers, headername="www-authenticate"):
"""Returns a dictionary of dictionaries, one dict per auth_scheme."""
header = headers.get(headername, "").strip()
if not header:
return {}
try:
parsed = www_authenticate.parseString(header)
except pp.ParseException as ex:
# print(ex.explain(ex))
raise MalformedHeader(headername)

retval = {
challenge["scheme"].lower(): challenge["params"].asDict()
if "params" in challenge
else {"token": challenge.get("token")}
for challenge in parsed
}
return retval
48 changes: 48 additions & 0 deletions python2/httplib2/error.py
@@ -0,0 +1,48 @@
# All exceptions raised here derive from HttpLib2Error
class HttpLib2Error(Exception):
pass


# Some exceptions can be caught and optionally
# be turned back into responses.
class HttpLib2ErrorWithResponse(HttpLib2Error):
def __init__(self, desc, response, content):
self.response = response
self.content = content
HttpLib2Error.__init__(self, desc)


class RedirectMissingLocation(HttpLib2ErrorWithResponse):
pass


class RedirectLimit(HttpLib2ErrorWithResponse):
pass


class FailedToDecompressContent(HttpLib2ErrorWithResponse):
pass


class UnimplementedDigestAuthOptionError(HttpLib2ErrorWithResponse):
pass


class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse):
pass


class MalformedHeader(HttpLib2Error):
pass


class RelativeURIError(HttpLib2Error):
pass


class ServerNotFoundError(HttpLib2Error):
pass


class ProxiesUnavailableError(HttpLib2Error):
pass

0 comments on commit b9a3c22

Please sign in to comment.