commit aa9d92d3a10505c3eeba2c0068b40f5aa4917ae2
parent 66a6cda6d8dec57a75904392c63a87b191a676d8
Author: Antoine A <>
Date: Thu, 9 Nov 2023 04:09:11 +0000
Limit the length of the request body and make deflate async
Diffstat:
3 files changed, 102 insertions(+), 22 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -46,9 +46,6 @@ import kotlinx.coroutines.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.*
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
-import kotlinx.serialization.encoding.encodeStructure
import kotlinx.serialization.json.*
import net.taler.common.errorcodes.TalerErrorCode
import org.slf4j.Logger
@@ -58,40 +55,58 @@ import tech.libeufin.util.CryptoUtil
import tech.libeufin.util.getVersion
import tech.libeufin.util.initializeDatabaseTables
import tech.libeufin.util.resetDatabaseTables
+import tech.libeufin.bank.libeufinError
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.zip.InflaterInputStream
+import java.util.zip.Inflater
+import java.util.zip.DataFormatException
import kotlin.system.exitProcess
// GLOBALS
private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.Main")
val TOKEN_DEFAULT_DURATION: java.time.Duration = Duration.ofDays(1L)
+private val MAX_BODY_LENGTH: Long = 4 * 1024 // 4kB
/**
- * This plugin inflates the requests that have "Content-Encoding: deflate"
+ * This plugin check for body lenght limit and inflates the requests that have "Content-Encoding: deflate"
*/
-val corebankDecompressionPlugin = createApplicationPlugin("RequestingBodyDecompression") {
+val bodyPlugin = createApplicationPlugin("BodyLimitAndDecompression") {
+ onCall {
+ val contentLenght = it.request.contentLength()
+ ?: throw badRequest("Missing Content-Length header", TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED)
+
+ if (contentLenght > MAX_BODY_LENGTH) {
+ throw badRequest("Body is suspiciously big")
+ }
+ }
onCallReceive { call ->
transformBody { data ->
if (call.request.headers[HttpHeaders.ContentEncoding] == "deflate") {
- logger.debug("Inflating request..")
- val brc = try {
- withContext(Dispatchers.IO) {
- val inflated = InflaterInputStream(data.toInputStream())
-
- @Suppress("BlockingMethodInNonBlockingContext")
- val bytes = inflated.readAllBytes()
- ByteReadChannel(bytes)
+ val inflater = Inflater()
+ val bytes = ByteArray(MAX_BODY_LENGTH.toInt())
+ var decoded = 0;
+
+ while (!inflater.finished()) {
+ if (decoded == bytes.size) {
+ throw badRequest("Decompressed body is suspiciously big")
+ }
+ data.read {
+ inflater.setInput(it)
+ try {
+ decoded += inflater.inflate(bytes, decoded, bytes.size - decoded)
+ } catch (e: DataFormatException) {
+ logger.error("Deflated request failed to inflate: ${e.message}")
+ throw badRequest(
+ "Could not inflate request",
+ TalerErrorCode.GENERIC_COMPRESSION_INVALID
+ )
+ }
}
- } catch (e: Exception) {
- logger.error("Deflated request failed to inflate: ${e.message}")
- throw badRequest(
- "Could not inflate request",
- TalerErrorCode.GENERIC_COMPRESSION_INVALID
- )
}
- brc
+
+ ByteReadChannel(bytes.copyOf(decoded))
} else data
}
}
@@ -117,7 +132,7 @@ fun Application.corebankWebApp(db: Database, ctx: BankConfig) {
allowMethod(HttpMethod.Delete)
allowCredentials = true
}
- install(corebankDecompressionPlugin)
+ install(bodyPlugin)
install(IgnoreTrailingSlash)
install(ContentNegotiation) {
json(Json {
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -914,7 +914,7 @@ class CoreBankWithdrawalApiTest {
// POST /withdrawals/withdrawal_id/confirm
@Test
- fun confirm() = bankSetup { db ->
+ fun confirm() = bankSetup { _ ->
// Check confirm created
client.post("/accounts/merchant/withdrawals") {
basicAuth("merchant", "merchant-password")
diff --git a/bank/src/test/kotlin/SecurityTest.kt b/bank/src/test/kotlin/SecurityTest.kt
@@ -0,0 +1,65 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2023 Taler Systems S.A.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+import org.junit.Test
+import org.postgresql.jdbc.PgConnection
+import tech.libeufin.bank.*
+import tech.libeufin.util.*
+import io.ktor.client.plugins.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.client.HttpClient
+import io.ktor.http.content.*
+import io.ktor.server.engine.*
+import io.ktor.server.testing.*
+import kotlin.test.*
+import kotlinx.coroutines.*
+
+class SecurityTest {
+ @Test
+ fun bodySizeLimit() = bankSetup { _ ->
+ val valid_req = json {
+ "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout"
+ "amount" to "KUDOS:0.3"
+ }
+ client.post("/accounts/merchant/transactions") {
+ basicAuth("merchant", "merchant-password")
+ jsonBody(valid_req)
+ }.assertNoContent()
+
+ // Check body too big
+ client.post("/accounts/merchant/transactions") {
+ basicAuth("merchant", "merchant-password")
+ jsonBody(valid_req) {
+ "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout${"A".repeat(4100)}"
+ }
+ }.assertBadRequest()
+
+ // Check body too big even after compression
+ client.post("/accounts/merchant/transactions") {
+ basicAuth("merchant", "merchant-password")
+ jsonBody(valid_req, deflate = true) {
+ "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout${"A".repeat(4100)}"
+ }
+ }.assertBadRequest()
+ }
+}
+
+
+