Skip to content

Commit bd4ecb9

Browse files
Merge commit from fork
* Start fixing client CRLF injection * Raise for invalid method * Update CHANGELOG.md
1 parent ca4252c commit bd4ecb9

6 files changed

Lines changed: 76 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [2.4.6] - 2025-12-??
8+
## [2.4.6] - 2026-01-13
99

10+
- Fix CRLF injection vulnerability in the BlackSheep HTTP Client, reported by Jinho Ju (@tr4ce-ju).
11+
- Add a `SECURITY.md` file.
1012
- Fix [#646](https://github.com/Neoteroi/BlackSheep/issues/646).
1113
- Modify the `Cookie` `repr` to not include the value in full, as it can contain secrets
1214
that would leak in logs.

SECURITY.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Security Policy
2+
3+
## Supported Versions
4+
5+
In general, due to limited maintainer bandwidth, only the latest version of
6+
BlackSheep is supported with patch releases. Exceptions may be made depending
7+
on the severity of the bug and the feasibility of backporting a fix to
8+
older releases.
9+
10+
## Reporting a Vulnerability
11+
12+
BlackSheep uses GitHub's security advisory functionality for private vulnerability
13+
reports. To make a private report, please use the "Report a vulnerability" button at
14+
[https://github.com/Neoteroi/BlackSheep/security/advisories](https://github.com/Neoteroi/BlackSheep/security/advisories).

blacksheep/scribe.pxd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ cdef bint is_small_response(Response response)
2727

2828
cdef bytes write_small_response(Response response)
2929

30+
cdef bytes _nocrlf(bytes value)
31+
3032
cdef void set_headers_for_content(Message message)
3133

3234
cdef void set_headers_for_response_content(Response message)

blacksheep/scribe.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,19 @@
88
MAX_RESPONSE_CHUNK_SIZE = 61440 # 64kb
99

1010

11+
def _nocrlf(value: bytes) -> bytes:
12+
"""Sanitize the given value to prevent CRLF injection."""
13+
return value.replace(b"\r", b"").replace(b"\n", b"")
14+
15+
1116
# Header writing utilities
1217
def write_header(header):
13-
return header[0] + b": " + header[1] + b"\r\n"
18+
"""
19+
This function writes a single HTTP header. It is used only by the HTTP Client part,
20+
because the server relies on the ASGI server to handle headers.
21+
"""
22+
# Sanitize header name and value to prevent CRLF injection
23+
return _nocrlf(header[0]) + b": " + _nocrlf(header[1]) + b"\r\n"
1424

1525

1626
def write_headers(headers):
@@ -43,28 +53,31 @@ def _get_status_line(status_code: int):
4353
}
4454

4555

46-
def get_status_line(status: int):
56+
def get_status_line(status: int) -> bytes:
4757
return STATUS_LINES[status]
4858

4959

50-
def write_request_method(request: Request):
60+
def write_request_method(request: Request) -> bytes:
61+
# RFC 7230: method must be a valid token
62+
if not re.match(r'^[!#$%&\'*+\-.0-9A-Z^_`a-z|~]+$', request.method):
63+
raise ValueError(f"Invalid HTTP method: {request.method!r}")
5164
return request.method.encode()
5265

5366

54-
def write_request_uri(request: Request):
67+
def write_request_uri(request: Request) -> bytes:
5568
url = request.url
56-
p = url.path or b"/"
69+
p = _nocrlf(url.path or b"/")
5770
if url.query:
58-
return p + b"?" + url.query
71+
return p + b"?" + _nocrlf(url.query)
5972
return p
6073

6174

62-
def ensure_host_header(request: Request):
75+
def ensure_host_header(request: Request) -> None:
6376
if request.url.host:
6477
request._add_header_if_missing(b"host", request.url.host)
6578

6679

67-
def should_use_chunked_encoding(content: Content):
80+
def should_use_chunked_encoding(content: Content) -> bool:
6881
return content.length < 0
6982

7083

blacksheep/scribe.pyx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,18 @@ from .url cimport URL
1010
cdef int MAX_RESPONSE_CHUNK_SIZE = 61440 # 64kb
1111

1212

13+
cdef bytes _nocrlf(bytes value):
14+
"""Sanitize the given value to prevent CRLF injection."""
15+
return value.replace(b"\r", b"").replace(b"\n", b"")
16+
17+
1318
cdef bytes write_header(tuple header):
14-
return header[0] + b': ' + header[1] + b'\r\n'
19+
"""
20+
This function writes a single HTTP header. It is used only by the HTTP Client part,
21+
because the server relies on the ASGI server to handle headers.
22+
"""
23+
# Sanitize header name and value to prevent CRLF injection
24+
return _nocrlf(header[0]) + b": " + _nocrlf(header[1]) + b"\r\n"
1525

1626

1727
cdef bytes write_headers(list headers):
@@ -48,15 +58,18 @@ cpdef bytes get_status_line(int status):
4858

4959

5060
cdef bytes write_request_method(Request request):
51-
return request.method.encode()
61+
# RFC 7230: method must be a valid token
62+
if not re.match(r'^[!#$%&\'*+\-.0-9A-Z^_`a-z|~]+$', request.method):
63+
raise ValueError(f"Invalid HTTP method: {request.method!r}")
64+
return _nocrlf(request.method.encode())
5265

5366

5467
cdef bytes write_request_uri(Request request):
5568
cdef bytes p
5669
cdef URL url = request.url
57-
p = url.path or b'/'
70+
p = _nocrlf(url.path or b'/')
5871
if url.query:
59-
return p + b'?' + url.query
72+
return p + b'?' + _nocrlf(url.query)
6073
return p
6174

6275

tests/test_requests.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,3 +651,22 @@ async def content_gen():
651651
def test_request_charset(content_type_header, expected_charset):
652652
request = Request("POST", b"/", [(b"Content-Type", content_type_header.encode())])
653653
assert request.charset == expected_charset
654+
655+
656+
def test_write_request_prevents_crlf_injection_in_headers():
657+
request = Request("GET", b"https://hello-world", [
658+
(b"X-Injection", b"Hello\nInjected: Bad"),
659+
])
660+
raw_bytes = write_small_request(request)
661+
662+
assert b"X-Injection: HelloInjected: Bad\r\n" in raw_bytes
663+
664+
665+
def test_write_request_prevents_crlf_injection_in_method():
666+
# Attempt to inject additional headers via CRLF in the HTTP method
667+
request = Request("GET\r\nX-Injected: Bad\r\nAnother: Header", b"https://hello-world", [])
668+
669+
with pytest.raises(ValueError) as exc_info:
670+
write_small_request(request)
671+
672+
assert "Invalid HTTP method" in str(exc_info.value)

0 commit comments

Comments
 (0)