HTTP request or response splitting with Netty header validation disabled

Keep Netty HTTP header validation enabled to prevent CRLF injection

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

java
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

java
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.

References