Skip to content

Commit

Permalink
HTTP2: Add protection against remote control frames that are triggere…
Browse files Browse the repository at this point in the history
…d by a remote peer (#9460)

Motivation:

Due how http2 spec is defined it is possible by a remote peer to flood us with frames that will trigger control frames as response, the problem here is that the remote peer can also just stop reading these (while still produce more of these) and so may drive us to the pointer where we either run out of memory or burn all CPU. To protect against this we need to implement some kind of limit that will tear down connections that cause the above mentioned situation.

See CVE-2019-9512 / CVE-2019-9514 / CVE-2019-9515

Modifications:

- Add Http2ControlFrameLimitEncoder which limits the number of queued control frames that were caused because of the remote peer.
- Allow to insert ths Http2ControlFrameLimitEncoder by setting AbstractHttp2ConnectionBuilder.encoderEnforceMaxQueuedControlFrames(...) to a number higher then 0. The default is 10000 which provides some protection by default but will hopefully not cause too many false-positives.
- Add unit tests

Result:

Protect against DDOS due control frames. Fixes CVE-2019-9512 / CVE-2019-9514 / CVE-2019-9515 .
  • Loading branch information
normanmaurer committed Aug 14, 2019
1 parent bc22bfa commit c6c6795
Show file tree
Hide file tree
Showing 6 changed files with 430 additions and 1 deletion.
Expand Up @@ -18,12 +18,12 @@

import io.netty.channel.Channel;
import io.netty.handler.codec.http2.Http2HeadersEncoder.SensitivityDetector;
import io.netty.util.internal.ObjectUtil;
import io.netty.util.internal.UnstableApi;

import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_LIST_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_RESERVED_STREAMS;
import static io.netty.handler.codec.http2.Http2PromisedRequestVerifier.ALWAYS_VERIFY;
import static io.netty.util.internal.ObjectUtil.checkPositive;
import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
import static java.util.Objects.requireNonNull;

Expand Down Expand Up @@ -107,6 +107,7 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
private Http2PromisedRequestVerifier promisedRequestVerifier = ALWAYS_VERIFY;
private boolean autoAckSettingsFrame = true;
private boolean autoAckPingFrame = true;
private int maxQueuedControlFrames = Http2CodecUtil.DEFAULT_MAX_QUEUED_CONTROL_FRAMES;

/**
* Sets the {@link Http2Settings} to use for the initial connection settings exchange.
Expand Down Expand Up @@ -325,6 +326,30 @@ protected B encoderEnforceMaxConcurrentStreams(boolean encoderEnforceMaxConcurre
return self();
}

/**
* Returns the maximum number of queued control frames that are allowed before the connection is closed.
* This allows to protected against various attacks that can lead to high CPU / memory usage if the remote-peer
* floods us with frames that would have us produce control frames, but stops to read from the underlying socket.
*
* {@code 0} means no protection is in place.
*/
protected int encoderEnforceMaxQueuedControlFrames() {
return maxQueuedControlFrames;
}

/**
* Sets the maximum number of queued control frames that are allowed before the connection is closed.
* This allows to protected against various attacks that can lead to high CPU / memory usage if the remote-peer
* floods us with frames that would have us produce control frames, but stops to read from the underlying socket.
*
* {@code 0} means no protection should be applied.
*/
protected B encoderEnforceMaxQueuedControlFrames(int maxQueuedControlFrames) {
enforceNonCodecConstraints("encoderEnforceMaxQueuedControlFrames");
this.maxQueuedControlFrames = ObjectUtil.checkPositiveOrZero(maxQueuedControlFrames, "maxQueuedControlFrames");
return self();
}

/**
* Returns the {@link SensitivityDetector} to use.
*/
Expand Down Expand Up @@ -470,6 +495,9 @@ private T buildFromConnection(Http2Connection connection) {
Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(connection, writer);
boolean encoderEnforceMaxConcurrentStreams = encoderEnforceMaxConcurrentStreams();

if (maxQueuedControlFrames != 0) {
encoder = new Http2ControlFrameLimitEncoder(encoder, maxQueuedControlFrames);
}
if (encoderEnforceMaxConcurrentStreams) {
if (connection.isServer()) {
encoder.close();
Expand Down
Expand Up @@ -132,6 +132,8 @@ public static long calculateMaxHeaderListSizeGoAway(long maxHeaderListSize) {

public static final long DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MILLIS = MILLISECONDS.convert(30, SECONDS);

public static final int DEFAULT_MAX_QUEUED_CONTROL_FRAMES = 10000;

/**
* Returns {@code true} if the stream is an outbound stream.
*
Expand Down
@@ -0,0 +1,113 @@
/*
* Copyright 2019 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.util.internal.ObjectUtil;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;

/**
* {@link DecoratingHttp2ConnectionEncoder} which guards against a remote peer that will trigger a massive amount
* of control frames but will not consume our responses to these.
* This encoder will tear-down the connection once we reached the configured limit to reduce the risk of DDOS.
*/
final class Http2ControlFrameLimitEncoder extends DecoratingHttp2ConnectionEncoder {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(Http2ControlFrameLimitEncoder.class);

private final int maxOutstandingControlFrames;
private final ChannelFutureListener outstandingControlFramesListener = new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
outstandingControlFrames--;
}
};
private Http2LifecycleManager lifecycleManager;
private int outstandingControlFrames;
private boolean limitReached;

Http2ControlFrameLimitEncoder(Http2ConnectionEncoder delegate, int maxOutstandingControlFrames) {
super(delegate);
this.maxOutstandingControlFrames = ObjectUtil.checkPositive(maxOutstandingControlFrames,
"maxOutstandingControlFrames");
}

@Override
public void lifecycleManager(Http2LifecycleManager lifecycleManager) {
this.lifecycleManager = lifecycleManager;
super.lifecycleManager(lifecycleManager);
}

@Override
public ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise) {
ChannelPromise newPromise = handleOutstandingControlFrames(ctx, promise);
if (newPromise == null) {
return promise;
}
return super.writeSettingsAck(ctx, newPromise);
}

@Override
public ChannelFuture writePing(ChannelHandlerContext ctx, boolean ack, long data, ChannelPromise promise) {
// Only apply the limit to ping acks.
if (ack) {
ChannelPromise newPromise = handleOutstandingControlFrames(ctx, promise);
if (newPromise == null) {
return promise;
}
return super.writePing(ctx, ack, data, newPromise);
}
return super.writePing(ctx, ack, data, promise);
}

@Override
public ChannelFuture writeRstStream(
ChannelHandlerContext ctx, int streamId, long errorCode, ChannelPromise promise) {
ChannelPromise newPromise = handleOutstandingControlFrames(ctx, promise);
if (newPromise == null) {
return promise;
}
return super.writeRstStream(ctx, streamId, errorCode, newPromise);
}

private ChannelPromise handleOutstandingControlFrames(ChannelHandlerContext ctx, ChannelPromise promise) {
if (!limitReached) {
if (outstandingControlFrames == maxOutstandingControlFrames) {
// Let's try to flush once as we may be able to flush some of the control frames.
ctx.flush();
}
if (outstandingControlFrames == maxOutstandingControlFrames) {
limitReached = true;
Http2Exception exception = Http2Exception.connectionError(Http2Error.ENHANCE_YOUR_CALM,
"Maximum number %d of outstanding control frames reached", maxOutstandingControlFrames);
logger.info("Maximum number {} of outstanding control frames reached. Closing channel {}",
maxOutstandingControlFrames, ctx.channel(), exception);

// First notify the Http2LifecycleManager and then close the connection.
lifecycleManager.onError(ctx, true, exception);
ctx.close();
}
outstandingControlFrames++;

// We did not reach the limit yet, add the listener to decrement the number of outstanding control frames
// once the promise was completed
return promise.unvoid().addListener(outstandingControlFramesListener);
}
return promise;
}
}
Expand Up @@ -120,6 +120,16 @@ public Http2FrameCodecBuilder encoderEnforceMaxConcurrentStreams(boolean encoder
return super.encoderEnforceMaxConcurrentStreams(encoderEnforceMaxConcurrentStreams);
}

@Override
public int encoderEnforceMaxQueuedControlFrames() {
return super.encoderEnforceMaxQueuedControlFrames();
}

@Override
public Http2FrameCodecBuilder encoderEnforceMaxQueuedControlFrames(int maxQueuedControlFrames) {
return super.encoderEnforceMaxQueuedControlFrames(maxQueuedControlFrames);
}

@Override
public Http2HeadersEncoder.SensitivityDetector headerSensitivityDetector() {
return super.headerSensitivityDetector();
Expand Down
Expand Up @@ -149,6 +149,16 @@ public Http2MultiplexCodecBuilder encoderEnforceMaxConcurrentStreams(boolean enc
return super.encoderEnforceMaxConcurrentStreams(encoderEnforceMaxConcurrentStreams);
}

@Override
public int encoderEnforceMaxQueuedControlFrames() {
return super.encoderEnforceMaxQueuedControlFrames();
}

@Override
public Http2MultiplexCodecBuilder encoderEnforceMaxQueuedControlFrames(int maxQueuedControlFrames) {
return super.encoderEnforceMaxQueuedControlFrames(maxQueuedControlFrames);
}

@Override
public Http2HeadersEncoder.SensitivityDetector headerSensitivityDetector() {
return super.headerSensitivityDetector();
Expand Down

0 comments on commit c6c6795

Please sign in to comment.