libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

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:
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 57++++++++++++++++++++++++++++++++++++---------------------
Mbank/src/test/kotlin/CoreBankApiTest.kt | 2+-
Abank/src/test/kotlin/SecurityTest.kt | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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() + } +} + + +