Description
Disabling Netty's HTTP header validation allows values containing actual CR/LF (\r\n) characters to be placed in headers. If untrusted values reach a request or response this way and a recipient interprets them as message boundaries, header injection or HTTP request/response splitting can result.
The literal string %0d%0a is not a newline. A step such as URL decoding must turn it into actual CR/LF before it reaches the header. The impact depends on the Netty pipeline and how proxies and recipients handle the message.
Potential impact
- Cookie or redirect manipulation through injected headers
- Response-content manipulation, XSS or cache poisoning, depending on recipient behavior
- Unintended backend requests where request splitting is possible
Remediation
- Use default constructors or configurations that enable validation. Netty 4.1 deprecates
DefaultHttpHeaders(boolean)in favor of the default constructor. - Keep header names under developer control and validate values against the field's syntax and length requirements. Reject CR/LF or handle them without corrupting the field's meaning. Colons are not forbidden in every header value.
- Validate after the final required decoding step and do not decode the validated value again. Check proxy protections as an additional control, not a replacement for application validation.
Examples
These Netty 4.1 excerpts use the URL-decoded name query parameter. Complete response-body and connection handling are omitted.
Before
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
public class UnsafeHeaderHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) {
// A query value that can contain CR/LF after URL decoding
String clientName = new QueryStringDecoder(req.uri()).parameters()
.getOrDefault("name", java.util.List.of("")).get(0);
// BAD: header validation is disabled
DefaultHttpResponse resp = new DefaultHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, false);
resp.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8");
// BAD: copying unvalidated input permits CRLF injection
resp.headers().set("X-Client-Name", clientName);
ctx.writeAndFlush(resp);
}
}
After
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
public class SafeHeaderHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private static String cleanHeaderValue(String v) {
if (v == null) return "";
// Remove CR/LF and limit length
String cleaned = v.replace("\r", "").replace("\n", "");
return cleaned.length() > 128 ? cleaned.substring(0, 128) : cleaned;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) {
String clientName = cleanHeaderValue(new QueryStringDecoder(req.uri()).parameters()
.getOrDefault("name", java.util.List.of("")).get(0));
// GOOD: the default constructor enables validation
DefaultHttpResponse resp = new DefaultHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
HttpHeaders headers = resp.headers();
headers.set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8");
headers.set("X-Client-Name", clientName);
ctx.writeAndFlush(resp);
}
}
The second example keeps header validation enabled and handles CR/LF and length after decoding. Depending on the header's meaning, rejecting an invalid request may be preferable to modifying its value.