libeufin

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

commit ef94b1db08bfe7c4cd8b37fabdd0d4d3f2403949
parent d22e513d6c9de8463f2c9655cf551af9ece329e2
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun,  9 Aug 2026 13:11:41 +0200

db: bound the negative page limit and cap the rows returned per query

'limit' had an upper bound but no lower one, and abs(Int.MIN_VALUE)
silently stays negative, so ?limit=-2147483648 reached Postgres as a
negative LIMIT.

Diffstat:
MAPI_CHANGES.md | 5+++++
Mlibeufin-common/src/main/kotlin/db/helpers.kt | 11++++++++++-
Mlibeufin-common/src/main/kotlin/params.kt | 5+++++
Mlibeufin-common/src/test/kotlin/ParamsTest.kt | 3+++
4 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/API_CHANGES.md b/API_CHANGES.md @@ -63,7 +63,12 @@ This files contains all the API changes for the current release: - Add GET /accounts/USERNAME/tokens - GET /accounts/USERNAME/tokens: add missing row_id field - GET /withdrawals/WITHDRAWAL_ID: add min_amount +- Paginated endpoints: reject a 'limit' (or 'delta') below -MAX_PAGE_SIZE, + mirroring the pre-existing upper bound ## bank cli ## nexus + +- Paginated endpoints: reject a 'limit' (or 'delta') below -MAX_PAGE_SIZE, + mirroring the pre-existing upper bound diff --git a/libeufin-common/src/main/kotlin/db/helpers.kt b/libeufin-common/src/main/kotlin/db/helpers.kt @@ -29,6 +29,13 @@ import tech.libeufin.common.PageParams import java.sql.PreparedStatement import java.sql.ResultSet import kotlin.math.abs +import kotlin.math.min + +/** + * Hard upper bound on the number of records returned by a single + * query, regardless of the limit requested by the client. + */ +private const val MAX_RECORDS: Long = 50_000 /** Apply paging logic to a sql query */ suspend fun <T> DbPool.page( @@ -48,7 +55,9 @@ suspend fun <T> DbPool.page( return serializable(pageQuery) { args() bind(params.offset) - bind(abs(params.limit)) + // Widen before abs(): abs(Int.MIN_VALUE) is Int.MIN_VALUE, which would + // reach Postgres as a negative LIMIT. + bind(min(MAX_RECORDS, abs(params.limit.toLong()))) all { map(it) } } } diff --git a/libeufin-common/src/main/kotlin/params.kt b/libeufin-common/src/main/kotlin/params.kt @@ -80,6 +80,11 @@ data class PageParams( val limit: Int = new_limit_value ?: legacy_limit_value ?: -20 if (limit == 0) throw paramsMalformed("Param 'limit' must be non-zero") else if (limit > MAX_PAGE_SIZE) throw paramsMalformed("Param 'limit' must be <= ${MAX_PAGE_SIZE}") + // The sign of 'limit' only selects the direction, so the negative + // side needs the same bound. Note that abs(Int.MIN_VALUE) silently + // returns Int.MIN_VALUE, so an unbounded negative limit would reach + // SQL as a negative LIMIT. + else if (limit < -MAX_PAGE_SIZE) throw paramsMalformed("Param 'limit' must be >= ${-MAX_PAGE_SIZE}") val offset: Long = new_offset_value ?: legacy_offset_value ?: if (limit >= 0) 0L else Long.MAX_VALUE if (offset < 0) throw paramsMalformed("Param 'offset' must be a positive number") diff --git a/libeufin-common/src/test/kotlin/ParamsTest.kt b/libeufin-common/src/test/kotlin/ParamsTest.kt @@ -48,6 +48,9 @@ class ParamsTest { "limit=${MAX_PAGE_SIZE}".check(0, MAX_PAGE_SIZE, 0) "limit=0".fail("Param 'limit' must be non-zero") "limit=${MAX_PAGE_SIZE+1}".fail("Param 'limit' must be <= ${MAX_PAGE_SIZE}") + "limit=${-MAX_PAGE_SIZE}".check(0, -MAX_PAGE_SIZE, Long.MAX_VALUE) + "limit=${-MAX_PAGE_SIZE-1}".fail("Param 'limit' must be >= ${-MAX_PAGE_SIZE}") + "limit=${Int.MIN_VALUE}".fail("Param 'limit' must be >= ${-MAX_PAGE_SIZE}") "offset=-1".fail("Param 'offset' must be a positive number") "long_poll_ms=${MAX_TIMEOUT_MS+1}&limit=10".check(MAX_TIMEOUT_MS, 10, 0) "long_poll_ms=1&timeout_ms=2".fail("Param 'timeout_ms' cannot be used with param 'long_poll_ms'")