res.writeHeader(key, value) writes key: value\r\n without looking at the value. A CR or an LF inside the value ends the header early, and everything after it is read by the client as more headers, or as a second response.
Node's http refuses the same value from setHeader with ERR_INVALID_CHAR, so a server that echoes a request value into a header is safe on node and split here.
Repro on v20.69.0, Node 26, nothing else involved:
uWS.App().get('/*', (res, req) => {
res.writeHeader('x-echo', decodeURIComponent(req.getQuery('v') || ''));
res.end('ok');
}).listen(9001, () => {});
Ask for /?v= with this value percent encoded:
a\r\nx-injected: yes\r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 25\r\n\r\n<script>alert(1)</script>
The wire answers two responses:
HTTP/1.1 200 OK
x-echo: a
x-injected: yes
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 25
<script>alert(1)</script>
Date: Sat, 15 Aug 2026 06:28:10 GMT
uWebSockets: 20
Content-Length: 2
ok
The header name is not checked either, so writeHeader(userInput, 'v') splits the same way. A lone LF also passes, and node's own client then rejects the whole response with HPE_CR_EXPECTED.
The request side is fine: uWS answers 400 to a CR or an LF in an incoming header, so this only happens with values the application puts there itself.
I know this is a low level API and a check per header costs something on a hot path. My point is that every framework built on uWS inherits this unless it validates on its own, and it is not written anywhere. Would you accept a check in writeHeader, or would a note in the README be better?
res.writeHeader(key, value)writeskey: value\r\nwithout looking at the value. A CR or an LF inside the value ends the header early, and everything after it is read by the client as more headers, or as a second response.Node's
httprefuses the same value fromsetHeaderwithERR_INVALID_CHAR, so a server that echoes a request value into a header is safe on node and split here.Repro on v20.69.0, Node 26, nothing else involved:
Ask for
/?v=with this value percent encoded:The wire answers two responses:
The header name is not checked either, so
writeHeader(userInput, 'v')splits the same way. A lone LF also passes, and node's own client then rejects the whole response with HPE_CR_EXPECTED.The request side is fine: uWS answers 400 to a CR or an LF in an incoming header, so this only happens with values the application puts there itself.
I know this is a low level API and a check per header costs something on a hot path. My point is that every framework built on uWS inherits this unless it validates on its own, and it is not written anywhere. Would you accept a check in writeHeader, or would a note in the README be better?