Skip to content

Commit 2fffabe

Browse files
authored
KTOR-4379 Validate body size equals Content-Length (#3069)
* KTOR-4379 Validate body size equals Content-Length
1 parent 7c32f4e commit 2fffabe

File tree

8 files changed

+122
-58
lines changed

8 files changed

+122
-58
lines changed

ktor-http/ktor-http-cio/common/src/io/ktor/http/cio/HttpBody.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ package io.ktor.http.cio
77
import io.ktor.http.*
88
import io.ktor.http.cio.internals.*
99
import io.ktor.utils.io.*
10-
10+
import io.ktor.utils.io.errors.*
1111
/**
1212
* @return `true` if an http upgrade is expected accoding to request [method], [upgrade] header value and
1313
* parsed [connectionOptions]
@@ -84,7 +84,11 @@ public suspend fun parseHttpBody(
8484
}
8585

8686
if (contentLength != -1L) {
87-
input.copyTo(out, contentLength)
87+
val size = input.copyTo(out, contentLength)
88+
89+
if (size != contentLength) {
90+
throw IOException("Unexpected body length: expected $contentLength, actual $size")
91+
}
8892
return
8993
}
9094

ktor-server/ktor-server-netty/jvm/src/io/ktor/server/netty/NettyApplicationCall.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ public abstract class NettyApplicationCall(
1919
private val requestMessage: Any,
2020
) : BaseApplicationCall(application) {
2121

22-
@OptIn(InternalAPI::class)
2322
public abstract override val request: NettyApplicationRequest
24-
@OptIn(InternalAPI::class)
2523
public abstract override val response: NettyApplicationResponse
2624

2725
internal lateinit var previousCallFinished: ChannelPromise
@@ -65,7 +63,6 @@ public abstract class NettyApplicationCall(
6563

6664
internal suspend fun finish() {
6765
try {
68-
@OptIn(InternalAPI::class)
6966
response.ensureResponseSent()
7067
} catch (cause: Throwable) {
7168
finishedEvent.setFailure(cause)
@@ -89,14 +86,12 @@ public abstract class NettyApplicationCall(
8986
}
9087
}
9188

92-
@OptIn(InternalAPI::class)
9389
private fun finishComplete() {
9490
responseWriteJob.cancel()
9591
request.close()
9692
releaseRequestMessage()
9793
}
9894

99-
@OptIn(InternalAPI::class)
10095
internal fun dispose() {
10196
response.close()
10297
request.close()

ktor-server/ktor-server-netty/jvm/src/io/ktor/server/netty/cio/RequestBodyHandler.kt

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
package io.ktor.server.netty.cio
66

77
import io.ktor.utils.io.*
8+
import io.ktor.utils.io.errors.*
89
import io.netty.buffer.*
910
import io.netty.channel.*
1011
import io.netty.handler.codec.http.*
1112
import io.netty.util.*
1213
import kotlinx.coroutines.*
1314
import kotlinx.coroutines.channels.Channel
15+
import java.lang.Integer.*
1416
import kotlin.coroutines.*
1517

18+
private class ChannelEvent(val channel: ByteWriteChannel, val expectedLength: Long)
19+
1620
internal class RequestBodyHandler(
1721
val context: ChannelHandlerContext
1822
) : ChannelInboundHandlerAdapter(), CoroutineScope {
@@ -26,36 +30,48 @@ internal class RequestBodyHandler(
2630

2731
private val job = launch(context.executor().asCoroutineDispatcher(), start = CoroutineStart.LAZY) {
2832
var current: ByteWriteChannel? = null
33+
var expectedLength = -1L
34+
var written = 0L
2935
var upgraded = false
3036

37+
fun checkCurrentLengthAndClose() {
38+
if (expectedLength == -1L || written == expectedLength) {
39+
current?.close()
40+
return
41+
}
42+
43+
val message = "Unexpected length of the request body. Expected $expectedLength but was $written"
44+
current?.close(IOException(message))
45+
}
46+
3147
try {
3248
while (true) {
33-
@OptIn(ExperimentalCoroutinesApi::class)
3449
val event = queue.tryReceive().getOrNull()
3550
?: run { current?.flush(); queue.receiveCatching().getOrNull() }
3651
?: break
3752

3853
when (event) {
3954
is ByteBufHolder -> {
40-
val channel = current
41-
?: throw IllegalStateException("No current channel but received a byte buf")
42-
processContent(channel, event)
55+
val channel = current ?: error("No current channel but received a byte buf")
56+
written += processContent(channel, event)
4357

4458
if (!upgraded && event is LastHttpContent) {
45-
current.close()
59+
checkCurrentLengthAndClose()
4660
current = null
4761
}
4862
requestMoreEvents()
4963
}
5064
is ByteBuf -> {
51-
val channel =
52-
current ?: throw IllegalStateException("No current channel but received a byte buf")
53-
processContent(channel, event)
65+
val channel = current ?: error("No current channel but received a byte buf")
66+
written += processContent(channel, event)
5467
requestMoreEvents()
5568
}
56-
is ByteWriteChannel -> {
57-
current?.close()
58-
current = event
69+
is ChannelEvent -> {
70+
checkCurrentLengthAndClose()
71+
72+
current = event.channel
73+
expectedLength = event.expectedLength
74+
written = 0L
5975
}
6076
is Upgrade -> {
6177
upgraded = true
@@ -66,7 +82,7 @@ internal class RequestBodyHandler(
6682
queue.close(t)
6783
current?.close(t)
6884
} finally {
69-
current?.close()
85+
checkCurrentLengthAndClose()
7086
queue.close()
7187
consumeAndReleaseQueue()
7288
}
@@ -75,7 +91,7 @@ internal class RequestBodyHandler(
7591
@OptIn(ExperimentalCoroutinesApi::class)
7692
fun upgrade(): ByteReadChannel {
7793
val result = queue.trySend(Upgrade)
78-
if (result.isSuccess) return newChannel()
94+
if (result.isSuccess) return newChannel(-1L)
7995

8096
if (queue.isClosedForSend) {
8197
throw CancellationException("HTTP pipeline has been terminated.", result.exceptionOrNull())
@@ -87,10 +103,10 @@ internal class RequestBodyHandler(
87103
)
88104
}
89105

90-
fun newChannel(): ByteReadChannel {
91-
val bc = ByteChannel()
92-
tryOfferChannelOrToken(bc)
93-
return bc
106+
fun newChannel(contentLength: Long): ByteReadChannel {
107+
val result = ByteChannel()
108+
tryOfferChannelOrToken(ChannelEvent(result, contentLength))
109+
return result
94110
}
95111

96112
@OptIn(ExperimentalCoroutinesApi::class)
@@ -121,18 +137,18 @@ internal class RequestBodyHandler(
121137
}
122138
}
123139

124-
private suspend fun processContent(current: ByteWriteChannel, event: ByteBufHolder) {
140+
private suspend fun processContent(current: ByteWriteChannel, event: ByteBufHolder): Int {
125141
try {
126142
val buf = event.content()
127-
copy(buf, current)
143+
return copy(buf, current)
128144
} finally {
129145
event.release()
130146
}
131147
}
132148

133-
private suspend fun processContent(current: ByteWriteChannel, buf: ByteBuf) {
149+
private suspend fun processContent(current: ByteWriteChannel, buf: ByteBuf): Int {
134150
try {
135-
copy(buf, current)
151+
return copy(buf, current)
136152
} finally {
137153
buf.release()
138154
}
@@ -160,12 +176,14 @@ internal class RequestBodyHandler(
160176
}
161177
}
162178

163-
private suspend fun copy(buf: ByteBuf, dst: ByteWriteChannel) {
179+
private suspend fun copy(buf: ByteBuf, dst: ByteWriteChannel): Int {
164180
val length = buf.readableBytes()
165181
if (length > 0) {
166182
val buffer = buf.internalNioBuffer(buf.readerIndex(), length)
167183
dst.writeFully(buffer)
168184
}
185+
186+
return max(length, 0)
169187
}
170188

171189
private fun handleBytesRead(content: ReferenceCounted) {

ktor-server/ktor-server-netty/jvm/src/io/ktor/server/netty/http1/NettyHttp1Handler.kt

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ internal class NettyHttp1Handler(
5151
*/
5252
internal val isChannelReadCompleted: AtomicBoolean = atomic(false)
5353

54-
@OptIn(InternalAPI::class)
5554
override fun channelActive(context: ChannelHandlerContext) {
5655
responseWriter = NettyHttpResponsePipeline(
5756
context,
@@ -152,17 +151,19 @@ internal class NettyHttp1Handler(
152151
)
153152
}
154153

155-
private fun prepareRequestContentChannel(context: ChannelHandlerContext, message: HttpRequest): ByteReadChannel {
156-
return when (message) {
157-
is HttpContent -> {
158-
val bodyHandler = context.pipeline().get(RequestBodyHandler::class.java)
159-
bodyHandler.newChannel().also { bodyHandler.channelRead(context, message) }
160-
}
161-
else -> {
162-
val bodyHandler = context.pipeline().get(RequestBodyHandler::class.java)
163-
bodyHandler.newChannel()
164-
}
154+
private fun prepareRequestContentChannel(
155+
context: ChannelHandlerContext,
156+
message: HttpRequest
157+
): ByteReadChannel {
158+
val bodyHandler = context.pipeline().get(RequestBodyHandler::class.java)
159+
val length = message.headers()[io.ktor.http.HttpHeaders.ContentLength]?.toLongOrNull() ?: -1
160+
val result = bodyHandler.newChannel(length)
161+
162+
if (message is HttpContent) {
163+
bodyHandler.channelRead(context, message)
165164
}
165+
166+
return result
166167
}
167168

168169
private fun callReadIfNeeded(context: ChannelHandlerContext) {

ktor-server/ktor-server-servlet/jvm/src/io/ktor/server/servlet/ServletReader.kt

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,10 @@ internal fun CoroutineScope.servletReader(input: ServletInputStream, contentLeng
2020
}
2121
}
2222

23-
private class ServletReader(val input: ServletInputStream, contentLength: Int) : ReadListener {
23+
private class ServletReader(val input: ServletInputStream, val contentLength: Int) : ReadListener {
2424
val channel = ByteChannel()
2525
private val events = Channel<Unit>(2)
2626

27-
private val contentLength: Int = if (contentLength < 0) Int.MAX_VALUE else contentLength
28-
2927
suspend fun run() {
3028
val buffer = ArrayPool.borrow()
3129
try {
@@ -72,20 +70,24 @@ private class ServletReader(val input: ServletInputStream, contentLength: Int) :
7270

7371
channel.writeFully(buffer, 0, readCount)
7472

73+
if (contentLength < 0) continue
74+
7575
if (bodySize == contentLength) {
7676
channel.close()
7777
events.close()
7878
break
7979
}
8080

81-
if (bodySize > contentLength) {
82-
val cause = IOException(
83-
"Client provided more bytes than content length. Expected $contentLength but got $bodySize."
84-
)
85-
channel.close(cause)
86-
events.close()
87-
break
81+
val message = if (bodySize > contentLength) {
82+
"Client provided more bytes than content length. Expected $contentLength but got $bodySize."
83+
} else {
84+
"Client provided less bytes than content length. Expected $contentLength but got $bodySize."
8885
}
86+
87+
val cause = IOException(message)
88+
channel.close(cause)
89+
events.close()
90+
break
8991
}
9092
}
9193

@@ -112,8 +114,7 @@ private class ServletReader(val input: ServletInputStream, contentLength: Int) :
112114
private fun wrapException(cause: Throwable): Throwable? {
113115
return when (cause) {
114116
is EOFException -> null
115-
is TimeoutException,
116-
is IOException -> ChannelReadException(
117+
is TimeoutException -> ChannelReadException(
117118
"Cannot read from a servlet input stream",
118119
exception = cause as Exception
119120
)

ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/ContentTestSuite.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -639,10 +639,10 @@ abstract class ContentTestSuite<TEngine : ApplicationEngine, TConfiguration : Ap
639639
call.receiveMultipart().forEachPart { part ->
640640
when (part) {
641641
is PartData.FormItem -> response.append("${part.name}=${part.value}\n")
642-
is PartData.FileItem -> response.append(
643-
"file:${part.name},${part.originalFileName}," +
644-
"${part.streamProvider().bufferedReader().lineSequence().count()}\n"
645-
)
642+
is PartData.FileItem -> {
643+
val lineSequence = part.streamProvider().bufferedReader().lineSequence()
644+
response.append("file:${part.name},${part.originalFileName},${lineSequence.count()}\n")
645+
}
646646
is PartData.BinaryItem -> {
647647
}
648648
is PartData.BinaryChannelItem -> {}

ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/SustainabilityTestSuite.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,49 @@ abstract class SustainabilityTestSuite<TEngine : ApplicationEngine, TConfigurati
846846
outputStream.close()
847847
}
848848
}
849+
850+
@Test
851+
fun testBodySmallerThanContentLength() {
852+
var failCause: Throwable? = null
853+
val result = Job()
854+
855+
createAndStartServer {
856+
post("/") {
857+
try {
858+
println(call.receive<ByteArray>().size)
859+
} catch (cause: Throwable) {
860+
failCause = cause
861+
} finally {
862+
result.complete()
863+
}
864+
865+
call.respond("OK")
866+
}
867+
}
868+
869+
socket {
870+
val request = buildString {
871+
append("POST / HTTP/1.1\r\n")
872+
append("Content-Length: 4\r\n")
873+
append("Content-Type: text/plain\r\n")
874+
append("Connection: close\r\n")
875+
append("Host: localhost\r\n")
876+
append("\r\n")
877+
append("ABC")
878+
}
879+
880+
outputStream.writer().use {
881+
it.write(request)
882+
}
883+
}
884+
885+
runBlocking {
886+
result.join()
887+
}
888+
889+
assertTrue(failCause != null)
890+
assertTrue(failCause is IOException)
891+
}
849892
}
850893

851894
internal inline fun assertFails(block: () -> Unit) {

ktor-utils/common/src/io/ktor/util/cio/Channels.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ public class ChannelWriteException(message: String = "Cannot write to a channel"
2323
* An exception that is thrown when an IO error occurred during reading from the request channel.
2424
* Usually it happens when a remote client closed the connection.
2525
*/
26-
public class ChannelReadException(message: String = "Cannot read from a channel", exception: Throwable) :
27-
ChannelIOException(message, exception)
26+
public class ChannelReadException(
27+
message: String = "Cannot read from a channel",
28+
exception: Throwable
29+
) : ChannelIOException(message, exception)

0 commit comments

Comments
 (0)