Skip to content

(permissive) request target validation #3373

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions gunicorn/http/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,38 @@
VERSION_RE = re.compile(r"HTTP/(\d)\.(\d)")
RFC9110_5_5_INVALID_AND_DANGEROUS = re.compile(r"[\0\r\n]")

RFC3986_2_URI_SPECIALS = (
# gen-delims
":/?#[]@"
# sub-delims
"!$&'()*+,;="
# for unreserved
"-._~"
# for pct-encoded
"%"
# notably absent from this list (must be pct-encoded):
# \N{SPACE}
# <> and {}
# ` a.k.a \N{GRAVE ACCENT}
# ^ a.k.a \N{CIRCUMFLEX ACCENT}
# | a.k.a \N{VERTICAL LINE}
# backslash a.k.a \N{REVERSE SOLIDUS}
)
GUNICORN_NONSTANDARD_URI_CHARACTERS = (
"\N{QUOTATION MARK}"
# firefox and curl do not consider pipe escape-worthy
"\N{VERTICAL LINE}"
# used in tests/requests/valid/027.http (utf8 decoded as latin-1)
# "\N{LATIN CAPITAL LETTER A WITH TILDE}"
# "\N{NO-BREAK SPACE}"
# any with significant bit set - includes the above
# also includes "\N{SOFT HYPHEN}"
# simplify this once util.bytes_to_str is deleted
+ bytes(range(0x80, 0xff + 1)).decode("latin-1")
)
GUNICORN_URI_SPECIALS = RFC3986_2_URI_SPECIALS + GUNICORN_NONSTANDARD_URI_CHARACTERS
URI_CHARACTERS_RE = re.compile(r"[%s0-9a-zA-Z]+" % (re.escape(GUNICORN_URI_SPECIALS)))


class Message:
def __init__(self, cfg, unreader, peer_addr):
Expand Down Expand Up @@ -425,6 +457,7 @@ def parse_request_line(self, line_bytes):
if self.cfg.casefold_http_method:
self.method = self.method.upper()

# https://datatracker.ietf.org/doc/html/rfc9112#section-3.2
# URI
self.uri = bits[1]

Expand All @@ -438,6 +471,9 @@ def parse_request_line(self, line_bytes):
# => manually reject one always invalid URI: empty
if len(self.uri) == 0:
raise InvalidRequestLine(bytes_to_str(line_bytes))
# => reject URI exceeding characters listed in RFC 3986
if not URI_CHARACTERS_RE.fullmatch(self.uri):
raise InvalidRequestLine(bytes_to_str(line_bytes))

try:
parts = split_request_uri(self.uri)
Expand Down
4 changes: 4 additions & 0 deletions tests/requests/invalid/nonascii_05.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
GET /one\0/two HTTP/1.1\r\n
Content-Length: 3\r\n
\r\n
WOW
3 changes: 3 additions & 0 deletions tests/requests/invalid/nonascii_05.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from gunicorn.http.errors import InvalidRequestLine

request = InvalidRequestLine
5 changes: 5 additions & 0 deletions tests/requests/valid/041.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
GET scheme+ext://user+ext:password!@[::1]:8000/path?query#frag HTTP/1.1\r\n
Host: localhost\r\n
CONTENT-LENGTH: 3\r\n
\r\n
odd
10 changes: 10 additions & 0 deletions tests/requests/valid/041.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
request = {
"method": "GET",
"uri": uri("scheme+ext://user+ext:password!@[::1]:8000/path?query#frag"),
"version": (1, 1),
"headers": [
("HOST", "localhost"),
("CONTENT-LENGTH", "3"),
],
"body": b'odd'
}
3 changes: 3 additions & 0 deletions tests/requests/valid/042.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
OPTIONS * HTTP/1.1\r\n
Content-Length: 0\r\n
\r\n
9 changes: 9 additions & 0 deletions tests/requests/valid/042.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
request = {
"method": "OPTIONS",
"uri": uri("*"),
"version": (1, 1),
"headers": [
("CONTENT-LENGTH", "0"),
],
"body": b''
}
Loading