From: Thomas Perale via buildroot <buildroot@buildroot.org>
To: buildroot@buildroot.org
Cc: "James Hilliard" <james.hilliard1@gmail.com>,
"Raphaël Mélotte" <raphael.melotte@mind.be>
Subject: [Buildroot] [PATCH 2025.02.x] package/python-autobahn: backport fix for GHSA-2gqc-mwv3-66rp
Date: Mon, 9 Nov 2026 15:28:11 +0100 [thread overview]
Message-ID: <20261109142811.424062-1-thomas.perale@mind.be> (raw)
This fixes the following vulnerability:
Autobahn parses the WebSocket opening handshake by decoding it as
ISO-8859-1 and calling Python str.splitlines(). str.splitlines() breaks
a string not only on CRLF but also on the bytes VT (0x0b), FF (0x0c), FS
(0x1c), GS (0x1d), RS (0x1e) and NEL (0x85), so a single header value
that contains one of those bytes is parsed as two separate,
application-visible headers. A reverse proxy that forwards an ordinary
header such as User-Agent without rejecting those bytes lets an
unauthenticated attacker smuggle a second header of their choosing into
the value, for example User-Agent: abc\x85X-Authenticated-User: admin.
When the application behind the proxy derives identity or authorization
from a proxy-injected header, the attacker forges that header and
bypasses the proxy's identity boundary.
For more information, see:
- https://github.com/crossbario/autobahn-python/security/advisories/GHSA-2gqc-mwv3-66rp
- https://github.com/crossbario/autobahn-python/commit/364b6d6dbad127f6aed8a51494755027bf968fc3
The backport is essentially the same as the upstream one but adapt to
the coding style in place for the version 24.4.2.
Tested with `./support/testing/run-tests -d dl -j9 -o output_folder tests.package.test_python_autobahn`.
Assisted-by: LLM
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
...arse-handshake-HTTP-headers-strictly.patch | 167 ++++++++++++++++++
1 file changed, 167 insertions(+)
create mode 100644 package/python-autobahn/0001-parse-handshake-HTTP-headers-strictly.patch
diff --git a/package/python-autobahn/0001-parse-handshake-HTTP-headers-strictly.patch b/package/python-autobahn/0001-parse-handshake-HTTP-headers-strictly.patch
new file mode 100644
index 0000000000..40c25c1ccc
--- /dev/null
+++ b/package/python-autobahn/0001-parse-handshake-HTTP-headers-strictly.patch
@@ -0,0 +1,167 @@
+From eb679b75ec34d92bfc0f169cf4af71163a7b128a Mon Sep 17 00:00:00 2001
+From: Tobias Oberstein <tobias.oberstein@gmail.com>
+Date: Tue, 3 Nov 2026 21:12:11 +0100
+Subject: [PATCH] #2034: GREEN - parse handshake HTTP headers strictly
+
+parseHttpHeader no longer uses str.splitlines(), which also split on VT,
+FF, FS, GS, RS, NEL and on a bare CR or LF, so a peer could make autobahn
+see header lines (e.g. X-Forwarded-For) that a front proxy treats as part
+of a header value.
+
+Now (RFC 9112 / RFC 9110), with the whole header block rejected
+(ValueError) otherwise:
+
+- lines are separated by CRLF only; a bare CR or LF is invalid;
+- a bare LF is rejected deliberately, although RFC 9112 2.2 allows
+ accepting it as a line end (a front proxy may not split on it);
+- no obs-fold; field name is a token, directly followed by the colon;
+- field values: VCHAR, obs-text, SP, HTAB only; trimmed of SP/HTAB only
+ (str.strip() also stripped NEL/NBSP);
+- no control characters in the status line;
+- a header line without a colon is rejected (was skipped silently).
+
+The client handshake and the proxy CONNECT now catch parse errors and fail
+the handshake / the proxy connect, like the server already did (HTTP 400).
+
+Tests: bare-LF comment, plus 2 tests (line without colon, empty name).
+Changelog: 26.11.1 entry (behaviour change).
+
+Local: twisted cpy311 417 OK / 28 skipped, pypy311 416 OK / 29 skipped;
+asyncio cpy311 383 passed, pypy311 382 passed (baseline + 47 new tests);
+ruff + ty 0.0.89 clean.
+
+Expected CI result: GREEN.
+
+Note: This work was completed with AI assistance (Claude Code).
+
+CVE: GHSA-2gqc-mwv3-66rp
+Upstream: https://github.com/crossbario/autobahn-python/commit/364b6d6dbad127f6aed8a51494755027bf968fc3
+[thomas:
+ - remove tests and changelog
+ - 24.4.2 has not type annotation and uses string with single-quote: `'`
+]
+Signed-off-by: Thomas Perale <thomas.perale@mind.be>
+---
+ autobahn/websocket/protocol.py | 76 ++++++++++++++++++++++++++--------
+ 1 file changed, 58 insertions(+), 18 deletions(-)
+
+diff --git a/autobahn/websocket/protocol.py b/autobahn/websocket/protocol.py
+index c4ef43f8..02ff926c 100755
+--- a/autobahn/websocket/protocol.py
++++ b/autobahn/websocket/protocol.py
+@@ -31,6 +31,7 @@ import struct
+ import random
+ import os
+ import pickle
++import re
+ import copy
+ import json
+ import time
+@@ -252,6 +253,18 @@ class FrameHeader(object):
+ self.mask = mask
+
+
++# RFC 9110 section 5.1: field-name = token
++_HTTP_FIELD_NAME = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+")
++
++# RFC 9110 section 5.5: a field value is made of VCHAR, obs-text (0x80-0xFF), SP
++# and HTAB. This matches any other character, i.e. CR, LF, NUL and all other
++# controls (CTL) and DEL.
++_HTTP_FIELD_VALUE_INVALID = re.compile(r"[\x00-\x08\x0a-\x1f\x7f]")
++
++# any control character (including HTAB) and DEL, not allowed in the start line
++_HTTP_START_LINE_INVALID = re.compile(r"[\x00-\x1f\x7f]")
++
++
+ def parseHttpHeader(data):
+ """
+ Parses the beginning of a HTTP request header (the data up to the \n\n line) into a pair
+@@ -273,27 +286,48 @@ def parseHttpHeader(data):
+ # - http://tools.ietf.org/html/rfc5987
+ # - https://github.com/crossbario/autobahn-python/issues/533
+ #
+- raw = data.decode('iso-8859-1').splitlines()
+- http_status_line = raw[0].strip()
++ text = data.decode('iso-8859-1')
++
++ # strip the terminating empty line; do NOT use str.splitlines(), which also
++ # splits on VT, FF, FS, GS, RS, NEL and on a bare CR or LF (#2034)
++ if text.endswith('\r\n\r\n'):
++ text = text[:-4]
++ elif text.endswith('\r\n'):
++ text = text[:-2]
++ raw = text.split('\r\n')
++
++ http_status_line = raw[0]
++ if _HTTP_START_LINE_INVALID.search(http_status_line):
++ raise ValueError('invalid character in HTTP status line')
++ http_status_line = http_status_line.strip(' ')
++
+ http_headers = {}
+ http_headers_cnt = {}
+ for h in raw[1:]:
++ if h[:1] in (' ', '\t'):
++ raise ValueError('obsolete line folding in HTTP header')
+ i = h.find(":")
+- if i > 0:
+- # HTTP header keys are case-insensitive
+- key = h[:i].strip().lower()
+- value = h[i + 1:].strip()
+-
+- # handle HTTP headers split across multiple lines
+- if key in http_headers:
+- http_headers[key] += ', {}'.format(value)
+- http_headers_cnt[key] += 1
+- else:
+- http_headers[key] = value
+- http_headers_cnt[key] = 1
++ if i < 0:
++ raise ValueError('HTTP header line without colon')
++ name = h[:i]
++ if not _HTTP_FIELD_NAME.fullmatch(name):
++ raise ValueError('invalid HTTP header field name')
++ # strip optional whitespace (OWS = SP / HTAB) only: str.strip() would
++ # also strip obs-text such as NEL or NBSP
++ value = h[i + 1:].strip(' \t')
++ if _HTTP_FIELD_VALUE_INVALID.search(value):
++ raise ValueError('invalid character in HTTP header field value')
++
++ # HTTP header keys are case-insensitive
++ key = name.lower()
++
++ # handle HTTP headers split across multiple lines
++ if key in http_headers:
++ http_headers[key] += ', {}'.format(value)
++ http_headers_cnt[key] += 1
+ else:
+- # skip bad HTTP header
+- pass
++ http_headers[key] = value
++ http_headers_cnt[key] = 1
+ return http_status_line, http_headers, http_headers_cnt
+
+
+@@ -3521,7 +3555,10 @@ class WebSocketClientProtocol(WebSocketProtocol):
+
+ # extract HTTP status line and headers
+ #
+- (http_status_line, http_headers, http_headers_cnt) = parseHttpHeader(http_response_data)
++ try:
++ (http_status_line, http_headers, http_headers_cnt) = parseHttpHeader(http_response_data)
++ except Exception as e:
++ return self.failProxyConnect("Error during parsing of HTTP status line / response headers : {0}".format(e))
+
+ # validate proxy connect response
+ #
+@@ -3726,7 +3763,10 @@ class WebSocketClientProtocol(WebSocketProtocol):
+
+ # extract HTTP status line and headers
+ #
+- (self.http_status_line, self.http_headers, http_headers_cnt) = parseHttpHeader(self.http_response_data)
++ try:
++ (self.http_status_line, self.http_headers, http_headers_cnt) = parseHttpHeader(self.http_response_data)
++ except Exception as e:
++ return self.failHandshake("Error during parsing of HTTP status line / response headers : {0}".format(e))
+
+ # validate WebSocket opening handshake server response
+ #
--
2.56.0
_______________________________________________
buildroot mailing list
buildroot@buildroot.org
https://lists.buildroot.org/mailman/listinfo/buildroot
reply other threads:[~2026-11-09 14:28 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261109142811.424062-1-thomas.perale@mind.be \
--to=buildroot@buildroot.org \
--cc=james.hilliard1@gmail.com \
--cc=raphael.melotte@mind.be \
--cc=thomas.perale@mind.be \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox