Nettyのヘッダー検証無効化によるHTTPリクエスト・レスポンス分割

NettyのHTTPヘッダー検証を有効にしてCRLFインジェクションを防ぐ

説明

NettyのHTTPヘッダー検証を無効にすると、実際のCR/LF(\r\n)を含む値をヘッダーに設定できます。信頼できない値がこの経路でリクエストやレスポンスに入り、受信側がメッセージの境界として解釈すると、ヘッダーインジェクションやHTTPリクエスト・レスポンス分割につながる可能性があります。

文字列%0d%0a自体は改行ではありません。URLデコードなどによって実際のCR/LFになった値がヘッダーに到達する必要があります。影響はNettyのパイプライン、プロキシ、受信側の処理によって変わります。

想定される影響

  • 追加ヘッダーによるCookie設定やリダイレクトの操作
  • 受信側の解釈に応じたレスポンス内容の改ざん、XSS、キャッシュ汚染
  • リクエスト分割が可能な構成での、意図しないバックエンドリクエスト

対処方法

  • 既定のコンストラクターなど、検証を有効にする構成を使ってください。Netty 4.1ではDefaultHttpHeaders(boolean)は非推奨で、既定のコンストラクターが推奨されています。
  • ヘッダー名は開発者が管理し、値はそのフィールドの構文と長さの要件に合わせて検証してください。CR/LFを拒否するか、意味を損なわない方法で処理します。コロンはすべてのヘッダー値で禁止されているわけではありません。
  • 必要なデコードがすべて終わった後に検証し、検証済みの値を再度デコードしないでください。プロキシの保護も確認しつつ、アプリケーションでの検証を省略しないでください。

例

Netty 4.1のヘッダー処理の抜粋です。URLデコードしたnameクエリ値を使い、レスポンス本文と接続処理の全体は省略しています。

変更前

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) {
        // URLデコード後にCR/LFを含む可能性のあるクエリ値
        String clientName = new QueryStringDecoder(req.uri()).parameters()
                .getOrDefault("name", java.util.List.of("")).get(0);

        // BAD: ヘッダー検証を無効化
        DefaultHttpResponse resp = new DefaultHttpResponse(
                HttpVersion.HTTP_1_1, HttpResponseStatus.OK, false);

        resp.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8");
        // BAD: 未検証の入力をコピーするとCRLF注入が可能
        resp.headers().set("X-Client-Name", clientName);

        ctx.writeAndFlush(resp);
    }
}

変更後

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 "";
        // CR/LFを除去し、長さを制限
        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: 既定のコンストラクターは検証を有効にする
        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);
    }
}

変更後はヘッダー検証を維持し、デコード後のCR/LFと長さを処理します。ヘッダーの意味によっては、不正な値を修正する代わりにリクエストを拒否する方法もあります。

参考資料