Skip to content

Commit 905559f

Browse files
authored
fix: Rate limit protection against rapid resets (#4324) (#4325)
1 parent ece6aa9 commit 905559f

File tree

6 files changed

+131
-9
lines changed

6 files changed

+131
-9
lines changed

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

Lines changed: 5 additions & 0 deletions
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‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import akka.event.LoggingAdapter
1010
importakka.http.impl.engine.{HttpConnectionIdleTimeoutBidi, HttpIdleTimeoutException }
1111
importakka.http.impl.engine.http2.FrameEvent._
1212
importakka.http.impl.engine.http2.client.ResponseParsing
13-
importakka.http.impl.engine.http2.framing.{FrameRenderer, Http2FrameParsing }
13+
importakka.http.impl.engine.http2.framing.{FrameRenderer, Http2FrameParsing, RSTFrameLimit }
1414
importakka.http.impl.engine.http2.hpack.{HeaderCompression, HeaderDecompression }
1515
importakka.http.impl.engine.parsing.HttpHeaderParser
1616
importakka.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-
defframing(log: LoggingAdapter):BidiFlow[FrameEvent, ByteString, ByteString, FrameEvent, NotUsed] =
171+
defframing(http2ServerSettings: Http2ServerSettings, log: LoggingAdapter):BidiFlow[FrameEvent, ByteString, ByteString, FrameEvent, NotUsed] =
172172
BidiFlow.fromFlows(
173173
Flow[FrameEvent].map(FrameRenderer.render),
174-
Flow[ByteString].via(newHttp2FrameParsing(shouldReadPreface =true, log)))
174+
Flow[ByteString].via(newHttp2FrameParsing(shouldReadPreface =true, log))
175+
.via(newRSTFrameLimit(http2ServerSettings))
176+
)
175177

176178
defframingClient(log: LoggingAdapter):BidiFlow[FrameEvent, ByteString, ByteString, FrameEvent, NotUsed] =
177179
BidiFlow.fromFlows(
Lines changed: 66 additions & 0 deletions
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+
packageakka.http.impl.engine.http2.framing
6+
7+
importakka.annotation.InternalApi
8+
importakka.http.impl.engine.http2.{FrameEvent, Http2Compliance }
9+
importakka.http.impl.engine.http2.FrameEvent.RstStreamFrame
10+
importakka.http.impl.engine.http2.Http2Protocol.ErrorCode
11+
importakka.http.scaladsl.settings.Http2ServerSettings
12+
importakka.stream.{Attributes, FlowShape, Inlet, Outlet }
13+
importakka.stream.stage.{GraphStage, GraphStageLogic, InHandler, OutHandler }
14+
15+
/**
16+
* INTERNAL API
17+
*/
18+
@InternalApi
19+
private[akka] finalclassRSTFrameLimit(http2ServerSettings: Http2ServerSettings) extendsGraphStage[FlowShape[FrameEvent, FrameEvent]]{
20+
21+
privatevalmaxResets= http2ServerSettings.maxResets
22+
privatevalmaxResetsIntervalNanos= http2ServerSettings.maxResetsInterval.toNanos
23+
24+
valin=Inlet[FrameEvent]("in")
25+
valout=Outlet[FrameEvent]("out")
26+
valshape=FlowShape(in, out)
27+
28+
overridedefcreateLogic(inheritedAttributes: Attributes):GraphStageLogic=newGraphStageLogic(shape) withInHandlerwithOutHandler{
29+
privatevarrstSeen=false
30+
privatevarrstCount=0
31+
privatevarrstSpanStartNanos=0L
32+
33+
setHandlers(in, out, this)
34+
35+
overridedefonPush():Unit={
36+
grab(in) match{
37+
caseframe: RstStreamFrame=>
38+
rstCount +=1
39+
valnow=System.nanoTime()
40+
if (!rstSeen){
41+
rstSeen =true
42+
rstSpanStartNanos = now
43+
push(out, frame)
44+
} elseif ((now - rstSpanStartNanos) <= maxResetsIntervalNanos){
45+
if (rstCount > maxResets){
46+
failStage(newHttp2Compliance.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+
overridedefonPull():Unit= pull(in)
65+
}
66+
}

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

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

4040
defgetPingTimeout:Duration=Duration.ofMillis(pingTimeout.toMillis)
4141
defwithPingTimeout(timeout: Duration):Http2ServerSettings= withPingTimeout(timeout.toMillis.millis)
42+
43+
defmaxResets:Int
44+
45+
defwithMaxResets(n: Int):Http2ServerSettings= copy(maxResets = n)
46+
47+
defgetMaxResetsInterval:Duration=Duration.ofMillis(maxResetsInterval.toMillis)
48+
49+
defwithMaxResetsInterval(interval: Duration):Http2ServerSettings= copy(maxResetsInterval = interval.toMillis.millis)
50+
4251
}
4352
objectHttp2ServerSettingsextendsSettingsCompanion[Http2ServerSettings]{
4453
defcreate(config: Config):Http2ServerSettings= scaladsl.settings.Http2ServerSettings(config)

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ trait Http2ServerSettings extends javadsl.settings.Http2ServerSettings with Http
8888
defpingTimeout:FiniteDuration
8989
defwithPingTimeout(timeout: FiniteDuration):Http2ServerSettings= copy(pingTimeout = timeout)
9090

91+
defmaxResets:Int
92+
93+
overridedefwithMaxResets(n: Int):Http2ServerSettings= copy(maxResets = n)
94+
95+
defmaxResetsInterval:FiniteDuration
96+
97+
defwithMaxResetsInterval(interval: FiniteDuration):Http2ServerSettings= copy(maxResetsInterval = interval)
98+
9199
@InternalApi
92100
private[http] definternalSettings: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
extendsHttp2ServerSettings{
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‎

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

3636
importscala.annotation.nowarn
3737
importjavax.net.ssl.SSLContext
3838
importorg.scalatest.concurrent.Eventually
3939
importorg.scalatest.concurrent.PatienceConfiguration.Timeout
4040

41+
importjava.nio.ByteOrder
4142
importscala.collection.immutable
4243
importscala.concurrent.duration._
4344
importscala.concurrent.Await
4445
importscala.concurrent.ExecutionContext
4546
importscala.concurrent.Future
4647
importscala.concurrent.Promise
48+
importscala.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(newTestSetup{
1693+
1694+
overridedefsettings: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+
overridedefhandlerFlow: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+
valrequest=HttpRequest(protocol =HttpProtocols.`HTTP/2.0`, uri ="/foo")
1705+
valerror= 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
implicitclassInWithStoppedStages(name: String){

0 commit comments

Comments
(0)