Skip to content

connectd: treat websocket handshake headers as case-insensitive#9308

Open
coreyphillips wants to merge 1 commit into
ElementsProject:masterfrom
coreyphillips:connectd-ws-case-insensitive-headers
Open

connectd: treat websocket handshake headers as case-insensitive#9308
coreyphillips wants to merge 1 commit into
ElementsProject:masterfrom
coreyphillips:connectd-ws-case-insensitive-headers

Conversation

@coreyphillips

Copy link
Copy Markdown

Fixes #9307.

The websocket handshake parser matches header names byte-exactly (get_http_hdr) and the Upgrade/Connection values with case-sensitive strstr. Header field names are case-insensitive per RFC 9110 section 5.1, and RFC 6455 says the Upgrade and Connection values are "treated as an ASCII case-insensitive value" (already quoted in the comment above http_respond).

Practical impact: Node.js's built-in WebSocket (undici) lowercases all request headers, so it can never connect to a bind-addr=ws: listener and gets "400 I only speak websocket". Browsers happen to send RFC casing and work.

Changes:

  • get_http_hdr matches header names case-insensitively (strncasecmp, as used elsewhere in the tree)
  • Upgrade/Connection values use a small case-insensitive contains helper
  • Sec-WebSocket-Version stays an exact "13" comparison and the key is base64 data, both unchanged
  • run-websocket.c gains a second handshake with lowercased header names and mixed-case values, as sent by Node; same Sec-WebSocket-Accept expected

Verified the failure mode against a live v26.06.1 bind-addr=ws: listener with raw replays (lowercased names rejected before, accepted with this change per the unit test vector).

@Andezion
Andezion force-pushed the connectd-ws-case-insensitive-headers branch from 880909c to 4438a15 Compare July 16, 2026 08:07
Andezion
Andezion previously approved these changes Jul 16, 2026

@Andezion Andezion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look good to me!

"sec-websocket-version: 13\r\n\r\n";

my_rbuf = tal_strdup(tmpctx, hdr);
my_rbuf_off = 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@Andezion
Andezion force-pushed the connectd-ws-case-insensitive-headers branch from ff22cee to 5710e47 Compare July 17, 2026 08:16
Comment thread connectd/websocketd.c Outdated
Comment on lines +134 to +143
static bool contains_ci(const char *haystack, const char *needle)
{
size_t n = strlen(needle);

for (const char *p = haystack; *p; p++) {
if (strncasecmp(p, needle, n) == 0)
return true;
}
return false;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use strcasestr as far as I can see, as pointed out in man 3 strstr :-)
It may also be more efficient as I think your example can be quadratic.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, thanks! I replaced the custom contains_ci() helper with strcasestr() and amended the commit.

Comment thread connectd/websocketd.c Outdated
if (memstarts(buf, hdrlen, hdrname, strlen(hdrname))
/* header name (ASCII case-insensitive, RFC 9110 #5.1)
* followed by : */
if (hdrlen > strlen(hdrname)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also memstarts_str which retains the length limit check on the buf, but loses the strlen on the headername. This is likely more robust than searching for the next \0 in the buf, which may overflow if I read this correctly (well the reader creating buf needs to take care of null-terminating the string.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! memstarts_str(a, al, s) expands to memstarts(a, al, s, strlen(s)), which bottoms out in a memcmp, a case-sensitive byte comparison, so from what I understand, using it here would lose the case-insensitive header-name matching this patch is fixing. I didn't find a case-insensitive counterpart in ccan (no memstarts_casestr or similar), so strncasecmp with an explicit length guard seemed like the closest safe equivalent.

On the overflow concern: strncasecmp never scans buf for a \0; it reads at most hdrnamelen bytes. strlen is only ever called on hdrname, which is a NUL-terminated literal. The hdrlen > hdrnamelen check guarantees the compared bytes and the following buf[hdrnamelen] == ':' check are within the current header line, so this doesn't rely on buf being NUL-terminated.

I did incorporate the two properties memstarts_str would have given us: the length check explicitly bounds access to buf, and strlen(hdrname) is hoisted out so it's computed once.

@coreyphillips
coreyphillips force-pushed the connectd-ws-case-insensitive-headers branch from 5710e47 to a523a9d Compare July 22, 2026 15:50
HTTP header field names are case-insensitive (RFC 9110 section 5.1) and
RFC 6455 says the Upgrade and Connection values are treated as ASCII
case-insensitive, as the comment in websocketd.c already quotes. The
handshake parser matched header names byte-exactly and the values with
case-sensitive strstr, so any client that normalizes header casing got
400: notably Node.js's built-in WebSocket (undici) lowercases all
request headers and can never connect to a bind-addr=ws: listener,
while browsers happen to send RFC casing and work.

Match header names case-insensitively in get_http_hdr and use a
case-insensitive search for the Upgrade and Connection values. The
Sec-WebSocket-Version value comparison stays exact and the key is
base64 data, unchanged. Extends the unit test with a lowercased
handshake as sent by Node.

Fixes ElementsProject#9307

Changelog-Fixed: connectd: the WebSocket port now accepts handshakes from clients that send lowercase HTTP header names, such as Node.js's built-in WebSocket.
Signed-off-by: Corey Phillips <corey.lyle.phillips@proton.me>
@coreyphillips
coreyphillips force-pushed the connectd-ws-case-insensitive-headers branch from a523a9d to 3e63246 Compare July 23, 2026 02:49
@coreyphillips
coreyphillips requested a review from cdecker July 23, 2026 02:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

connectd websocket: HTTP handshake headers matched case-sensitively, rejecting RFC-compliant clients

3 participants