Commit
Fixes CPU burn DoS by cubic complexity of whitespace matching in WWW_AUTH_RELAXED (default) regexp.
- Loading branch information
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import base64 | ||
import re | ||
|
||
import pyparsing as pp | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
temoto
Author
Member
|
||
|
||
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") | ||
This comment has been minimized.
Sorry, something went wrong.
ptmcg
Contributor
|
||
|
||
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) | ||
This comment has been minimized.
Sorry, something went wrong.
ptmcg
Contributor
|
||
params = pp.Dict(pp.delimitedList(pp.Group(auth_param))) | ||
|
||
scheme = token("scheme") | ||
challenge = scheme + (token68("token") ^ params("params")) | ||
This comment has been minimized.
Sorry, something went wrong.
ptmcg
Contributor
|
||
|
||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
2 comments
on commit bd9ee25
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm glad pyparsing was useful to you in resolving this. I had to fix similar regex parsing issues (runaway backtracking) in some of pyparsing's internal regexen, so I can definitely empathize!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mean improvement on realistic and synthetic complex headers is around 50%. Thank you very much.
pyparsing 2.4.7 is Py2 and Py3 compatible. It may be worth splitting the parser out to a separate module that both your P2 and Py3 versions import.