Skip to content

Commit ebd0599

Browse files
committed
fix: Rate limit protection against rapid resets (#4324)
1 parent ece6aa9 commit ebd0599

File tree

6 files changed

+131
-9
lines changed

6 files changed

+131
-9
lines changed

akka-http-core/src/main/resources/reference.conf

+5
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,11 @@ akka.http {
309309
# Fail the connection if a sent ping is not acknowledged within this timeout.
310310
# When zero the ping-interval is used, if set the value must be evenly divisible by less than or equal to the ping-interval.
311311
ping-timeout = 0s
312+
313+
# Limit the number of RSTs a client is allowed to do on one connection, per interval
314+
# Protects against rapid reset attacks. If a connection goes over the limit, it is closed with HTTP/2 protocol error ENHANCE_YOUR_CALM
315+
max-resets = 400
316+
max-resets-interval = 10s
312317
}
313318

314319
websocket {

akka-http-core/src/main/scala/akka/http/impl/engine/http2/Http2Blueprint.scala

+6-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import akka.event.LoggingAdapter
1010
import akka.http.impl.engine.{ HttpConnectionIdleTimeoutBidi, HttpIdleTimeoutException }
1111
import akka.http.impl.engine.http2.FrameEvent._
1212
import akka.http.impl.engine.http2.client.ResponseParsing
13-
import akka.http.impl.engine.http2.framing.{ FrameRenderer, Http2FrameParsing }
13+
import akka.http.impl.engine.http2.framing.{ FrameRenderer, Http2FrameParsing, RSTFrameLimit }
1414
import akka.http.impl.engine.http2.hpack.{ HeaderCompression, HeaderDecompression }
1515
import akka.http.impl.engine.parsing.HttpHeaderParser
1616
import akka.http.impl.engine.rendering.DateHeaderRendering
@@ -108,7 +108,7 @@ private[http] object Http2Blueprint {
108108
serverDemux(settings.http2Settings, initialDemuxerSettings, upgraded) atop
109109
FrameLogger.logFramesIfEnabled(settings.http2Settings.logFrames) atop // enable for debugging
110110
hpackCoding(masterHttpHeaderParser, settings.parserSettings) atop
111-
framing(log) atop
111+
framing(settings.http2Settings, log) atop
112112
errorHandling(log) atop
113113
idleTimeoutIfConfigured(settings.idleTimeout)
114114
}
@@ -168,10 +168,12 @@ private[http] object Http2Blueprint {
168168
Flow[ByteString]
169169
)
170170

171-
def framing(log: LoggingAdapter): BidiFlow[FrameEvent, ByteString, ByteString, FrameEvent, NotUsed] =
171+
def framing(http2ServerSettings: Http2ServerSettings, log: LoggingAdapter): BidiFlow[FrameEvent, ByteString, ByteString, FrameEvent, NotUsed] =
172172
BidiFlow.fromFlows(
173173
Flow[FrameEvent].map(FrameRenderer.render),
174-
Flow[ByteString].via(new Http2FrameParsing(shouldReadPreface = true, log)))
174+
Flow[ByteString].via(new Http2FrameParsing(shouldReadPreface = true, log))
175+
.via(new RSTFrameLimit(http2ServerSettings))
176+
)
175177

176178
def framingClient(log: LoggingAdapter): BidiFlow[FrameEvent, ByteString, ByteString, FrameEvent, NotUsed] =
177179
BidiFlow.fromFlows(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (C) 2023 Lightbend Inc. <https://www.lightbend.com>
3+
*/
4+
5+
package akka.http.impl.engine.http2.framing
6+
7+
import akka.annotation.InternalApi
8+
import akka.http.impl.engine.http2.{ FrameEvent, Http2Compliance }
9+
import akka.http.impl.engine.http2.FrameEvent.RstStreamFrame
10+
import akka.http.impl.engine.http2.Http2Protocol.ErrorCode
11+
import akka.http.scaladsl.settings.Http2ServerSettings
12+
import akka.stream.{ Attributes, FlowShape, Inlet, Outlet }
13+
import akka.stream.stage.{ GraphStage, GraphStageLogic, InHandler, OutHandler }
14+
15+
/**
16+
* INTERNAL API
17+
*/
18+
@InternalApi
19+
private[akka] final class RSTFrameLimit(http2ServerSettings: Http2ServerSettings) extends GraphStage[FlowShape[FrameEvent, FrameEvent]] {
20+
21+
private val maxResets = http2ServerSettings.maxResets
22+
private val maxResetsIntervalNanos = http2ServerSettings.maxResetsInterval.toNanos
23+
24+
val in = Inlet[FrameEvent]("in")
25+
val out = Outlet[FrameEvent]("out")
26+
val shape = FlowShape(in, out)
27+
28+
override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) with InHandler with OutHandler {
29+
private var rstSeen = false
30+
private var rstCount = 0
31+
private var rstSpanStartNanos = 0L
32+
33+
setHandlers(in, out, this)
34+
35+
override def onPush(): Unit = {
36+
grab(in) match {
37+
case frame: RstStreamFrame =>
38+
rstCount += 1
39+
val now = System.nanoTime()
40+
if (!rstSeen) {
41+
rstSeen = true
42+
rstSpanStartNanos = now
43+
push(out, frame)
44+
} else if ((now - rstSpanStartNanos) <= maxResetsIntervalNanos) {
45+
if (rstCount > maxResets) {
46+
failStage(new Http2Compliance.Http2ProtocolException(
47+
ErrorCode.ENHANCE_YOUR_CALM,
48+
s"Too many RST frames per second for this connection. (Configured limit ${maxResets}/${http2ServerSettings.maxResetsInterval.toCoarsest})"))
49+
} else {
50+
push(out, frame)
51+
}
52+
} else {
53+
// outside time window, reset counter
54+
rstCount = 1
55+
rstSpanStartNanos = now
56+
push(out, frame)
57+
}
58+
59+
case frame =>
60+
push(out, frame)
61+
}
62+
}
63+
64+
override def onPull(): Unit = pull(in)
65+
}
66+
}

akka-http-core/src/main/scala/akka/http/javadsl/settings/Http2ServerSettings.scala

+9
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ trait Http2ServerSettings { self: scaladsl.settings.Http2ServerSettings with akk
3939

4040
def getPingTimeout: Duration = Duration.ofMillis(pingTimeout.toMillis)
4141
def withPingTimeout(timeout: Duration): Http2ServerSettings = withPingTimeout(timeout.toMillis.millis)
42+
43+
def maxResets: Int
44+
45+
def withMaxResets(n: Int): Http2ServerSettings = copy(maxResets = n)
46+
47+
def getMaxResetsInterval: Duration = Duration.ofMillis(maxResetsInterval.toMillis)
48+
49+
def withMaxResetsInterval(interval: Duration): Http2ServerSettings = copy(maxResetsInterval = interval.toMillis.millis)
50+
4251
}
4352
object Http2ServerSettings extends SettingsCompanion[Http2ServerSettings] {
4453
def create(config: Config): Http2ServerSettings = scaladsl.settings.Http2ServerSettings(config)

akka-http-core/src/main/scala/akka/http/scaladsl/settings/Http2ServerSettings.scala

+15-2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ trait Http2ServerSettings extends javadsl.settings.Http2ServerSettings with Http
8888
def pingTimeout: FiniteDuration
8989
def withPingTimeout(timeout: FiniteDuration): Http2ServerSettings = copy(pingTimeout = timeout)
9090

91+
def maxResets: Int
92+
93+
override def withMaxResets(n: Int): Http2ServerSettings = copy(maxResets = n)
94+
95+
def maxResetsInterval: FiniteDuration
96+
97+
def withMaxResetsInterval(interval: FiniteDuration): Http2ServerSettings = copy(maxResetsInterval = interval)
98+
9199
@InternalApi
92100
private[http] def internalSettings: Option[Http2InternalServerSettings]
93101
@InternalApi
@@ -110,7 +118,10 @@ object Http2ServerSettings extends SettingsCompanion[Http2ServerSettings] {
110118
logFrames: Boolean,
111119
pingInterval: FiniteDuration,
112120
pingTimeout: FiniteDuration,
113-
internalSettings: Option[Http2InternalServerSettings])
121+
maxResets: Int,
122+
maxResetsInterval: FiniteDuration,
123+
internalSettings: Option[Http2InternalServerSettings]
124+
)
114125
extends Http2ServerSettings {
115126
require(maxConcurrentStreams >= 0, "max-concurrent-streams must be >= 0")
116127
require(requestEntityChunkSize > 0, "request-entity-chunk-size must be > 0")
@@ -134,7 +145,9 @@ object Http2ServerSettings extends SettingsCompanion[Http2ServerSettings] {
134145
logFrames = c.getBoolean("log-frames"),
135146
pingInterval = c.getFiniteDuration("ping-interval"),
136147
pingTimeout = c.getFiniteDuration("ping-timeout"),
137-
None // no possibility to configure internal settings with config
148+
maxResets = c.getInt("max-resets"),
149+
maxResetsInterval = c.getFiniteDuration("max-resets-interval"),
150+
internalSettings = None, // no possibility to configure internal settings with config
138151
)
139152
}
140153
}

akka-http2-support/src/test/scala/akka/http/impl/engine/http2/Http2ServerSpec.scala

+30-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import akka.http.impl.engine.http2.Http2Protocol.ErrorCode
1212
import akka.http.impl.engine.http2.Http2Protocol.Flags
1313
import akka.http.impl.engine.http2.Http2Protocol.FrameType
1414
import akka.http.impl.engine.http2.Http2Protocol.SettingIdentifier
15+
import akka.http.impl.engine.http2.framing.FrameRenderer
1516
import akka.http.impl.engine.server.{ HttpAttributes, ServerTerminator }
1617
import akka.http.impl.engine.ws.ByteStringSinkProbe
1718
import akka.http.impl.util.AkkaSpecWithMaterializer
@@ -22,28 +23,29 @@ import akka.http.scaladsl.model._
2223
import akka.http.scaladsl.model.headers.CacheDirectives
2324
import akka.http.scaladsl.model.headers.RawHeader
2425
import akka.http.scaladsl.settings.ServerSettings
25-
import akka.stream.Attributes
26+
import akka.stream.{ Attributes, DelayOverflowStrategy, OverflowStrategy }
2627
import akka.stream.Attributes.LogLevels
27-
import akka.stream.OverflowStrategy
2828
import akka.stream.scaladsl.{ BidiFlow, Flow, Keep, Sink, Source, SourceQueueWithComplete }
2929
import akka.stream.testkit.TestPublisher.{ ManualProbe, Probe }
3030
import akka.stream.testkit.scaladsl.StreamTestKit
3131
import akka.stream.testkit.TestPublisher
3232
import akka.stream.testkit.TestSubscriber
3333
import akka.testkit._
34-
import akka.util.ByteString
34+
import akka.util.{ ByteString, ByteStringBuilder }
3535

3636
import scala.annotation.nowarn
3737
import javax.net.ssl.SSLContext
3838
import org.scalatest.concurrent.Eventually
3939
import org.scalatest.concurrent.PatienceConfiguration.Timeout
4040

41+
import java.nio.ByteOrder
4142
import scala.collection.immutable
4243
import scala.concurrent.duration._
4344
import scala.concurrent.Await
4445
import scala.concurrent.ExecutionContext
4546
import scala.concurrent.Future
4647
import scala.concurrent.Promise
48+
import scala.util.Success
4749

4850
/**
4951
* This tests the http2 server protocol logic.
@@ -1686,6 +1688,31 @@ class Http2ServerSpec extends AkkaSpecWithMaterializer("""
16861688
terminated.futureValue
16871689
}
16881690
}
1691+
1692+
"not allow high a frequency of resets for one connection" in StreamTestKit.assertAllStagesStopped(new TestSetup {
1693+
1694+
override def settings: ServerSettings = super.settings.withHttp2Settings(super.settings.http2Settings.withMaxResets(100).withMaxResetsInterval(2.seconds))
1695+
1696+
// covers CVE-2023-44487 with a rapid sequence of RSTs
1697+
override def handlerFlow: Flow[HttpRequest, HttpResponse, NotUsed] = Flow[HttpRequest].buffer(1000, OverflowStrategy.backpressure).mapAsync(300) { req =>
1698+
// never actually reached since rst is in headers
1699+
req.entity.discardBytes()
1700+
Future.successful(HttpResponse(entity = "Ok").withAttributes(req.attributes))
1701+
}
1702+
1703+
network.toNet.request(100000L)
1704+
val request = HttpRequest(protocol = HttpProtocols.`HTTP/2.0`, uri = "/foo")
1705+
val error = intercept[AssertionError] {
1706+
for (streamId <- 1 to 300 by 2) {
1707+
network.sendBytes(
1708+
FrameRenderer.render(HeadersFrame(streamId, true, true, network.encodeRequestHeaders(request), None))
1709+
++ FrameRenderer.render(RstStreamFrame(streamId, ErrorCode.CANCEL))
1710+
)
1711+
}
1712+
}
1713+
error.getMessage should include("Too many RST frames per second for this connection.")
1714+
network.toNet.cancel()
1715+
})
16891716
}
16901717

16911718
implicit class InWithStoppedStages(name: String) {

0 commit comments

Comments
 (0)