libdbi-perl: Fix CVE-2026-14739

Backport the upstream hard limit for positional placeholders. This is
a follow-up to CVE-2026-10879 and depends on the allocation fix from
the preceding libdbi-perl commit.

Correct the upstream boundary check so that the documented maximum of
99999 placeholders is accepted and values above it are rejected. Add
focused regression coverage for the 99999 and 100000 boundaries.

[1] https://github.com/perl5-dbi/dbi/commit/2b77c88b655e9539a592c71a61fb965fc0075395
[2] https://github.com/perl5-dbi/dbi/commit/af79036c07aa9a457971c0f4136e37c85dc20978
[3] https://nvd.nist.gov/vuln/detail/CVE-2026-14739
[4] https://nvd.nist.gov/vuln/detail/CVE-2026-10879

Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
Hetvi Thakar
2026-07-29 02:25:54 -07:00
committed by Anuj Mittal
parent dacd50f84e
commit 3dd0a40ecb
2 changed files with 171 additions and 0 deletions
@@ -0,0 +1,170 @@
From 57d0a1f66b9ed1b54f11af661da4719c43409497 Mon Sep 17 00:00:00 2001
From: "H.Merijn Brand - Tux" <linux@tux.freedom.nl>
Date: Sat, 4 Jul 2026 11:38:24 +0200
Subject: [PATCH] Set a hard limit of 99999 on '?' placeholders
(CVE-2026-14739)
CVE: CVE-2026-14739
Upstream-Status: Backport [https://github.com/perl5-dbi/dbi/commit/2b77c88b655e9539a592c71a61fb965fc0075395]
Backport Changes:
- Omit ChangeLog, dbixs_rev.h, lib/DBI/Changes.pm, and the DBI.pm version-number hunks because the target remains DBI 1.643.
- Omit regenerated doc/DBI.3, doc/DBI.html, doc/DBI.man, and doc/DBI.md files; retain the DBI.pm source-documentation and DBI.xs security changes unchanged.
- Correct the upstream boundary check to accept exactly 99999 placeholders and reject values above that documented limit; add t/60preparse.t regression coverage for 99999 and 100000 placeholders.
(cherry picked from commit 2b77c88b655e9539a592c71a61fb965fc0075395)
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
---
DBI.pm | 6 ++++--
DBI.xs | 53 +++++++++++++++++++++++++++++++++--------------------
t/60preparse.t | 13 ++++++++++++-
3 files changed, 49 insertions(+), 23 deletions(-)
diff --git a/DBI.pm b/DBI.pm
index d62a32d..b988e40 100644
--- a/DBI.pm
+++ b/DBI.pm
@@ -7123,7 +7123,8 @@ a ref to an empty hash because they can't pre-determine the names.
It is possible that the keys in the hash returned by C<ParamValues>
are not exactly the same as those implied by the prepared statement.
For example, DBD::Oracle translates 'C<?>' placeholders into 'C<:pN>'
-where N is a sequence number starting at 1.
+where N is a sequence number starting at C<1> with a hard limit of
+C<99999>.
* Values:
@@ -7225,7 +7226,8 @@ integer.
It is also possible that the keys in the hash returned by
C<ParamArrays> are not exactly the same as those implied by the
prepared statement. For example, DBD::Oracle translates 'C<?>'
-placeholders into 'C<:pN>' where N is a sequence number starting at 1.
+placeholders into 'C<:pN>' where N is a sequence number starting at
+C<1> with a hard limit of C<99999>.
=head3 C<RowsInCache>
diff --git a/DBI.xs b/DBI.xs
index 8858e21..23ad34a 100644
--- a/DBI.xs
+++ b/DBI.xs
@@ -4201,7 +4201,14 @@ preparse(SV *dbh, const char *statement, IV ps_return, IV ps_accept, void *foo)
}
/* XXX this allocation strategy won't work when we get to more advanced stuff */
- new_stmt_sv = newSV(strlen(statement) * 6 + 16);
+ /* The 7 is for length increase from '?' (length 1) to :p99999 (length 7)
+ * which imposes a limit of 99999 '?' placeholders POSIX style. Actual counts
+ * are a bit higher:
+ * using factor 5: :p1 .. :p1107
+ * using factor 6: :p1 .. :p11106
+ * using factor 7: :p1 .. :p111105
+ * and that count is insane already */
+ new_stmt_sv = newSV(strlen(statement) * 7 + 16);
sv_setpv(new_stmt_sv,"");
src = statement;
dest = SvPVX(new_stmt_sv);
@@ -4340,9 +4347,9 @@ preparse(SV *dbh, const char *statement, IV ps_return, IV ps_accept, void *foo)
continue;
}
- if ( !(*src==':' && (PS_accept(DBIpp_ph_cn) || PS_accept(DBIpp_ph_cs)))
- && !(*src=='?' && PS_accept(DBIpp_ph_qm))
- ){
+ if ( !(*src==':' && (PS_accept(DBIpp_ph_cn) || PS_accept(DBIpp_ph_cs)))
+ && !(*src=='?' && PS_accept(DBIpp_ph_qm))
+ ){
if (*src == '\'' || *src == '"')
in_quote = *src;
*dest++ = *src++;
@@ -4361,12 +4368,18 @@ preparse(SV *dbh, const char *statement, IV ps_return, IV ps_accept, void *foo)
if (PS_return(DBIpp_ph_qm))
;
else if (PS_return(DBIpp_ph_cn)) { /* '?' -> ':p1' (etc) */
+ if (idx > 99999) {
+ char buf[99];
+ sprintf(buf, "preparse found more than 99999 '?' placeholders. Limit exceeded.");
+ set_err_char(dbh, imp_xxh, "1", 1, buf, 0, "preparse");
+ return &PL_sv_undef;
+ }
sprintf(start,":p%d", idx++);
dest = start+strlen(start);
}
else if (PS_return(DBIpp_ph_sp)) { /* '?' -> '%s' */
- *start = '%';
- *dest++ = 's';
+ *start = '%';
+ *dest++ = 's';
}
}
else if (isDIGIT(*src)) { /* :1 */
@@ -4374,24 +4387,24 @@ preparse(SV *dbh, const char *statement, IV ps_return, IV ps_accept, void *foo)
style = ":1";
if (PS_return(DBIpp_ph_cn)) { /* ':1'->':p1' */
- idx = pln;
- *dest++ = 'p';
- while(isDIGIT(*src))
- *dest++ = *src++;
+ idx = pln;
+ *dest++ = 'p';
+ while(isDIGIT(*src))
+ *dest++ = *src++;
}
else if (PS_return(DBIpp_ph_qm) /* ':1' -> '?' */
|| PS_return(DBIpp_ph_sp) /* ':1' -> '%s' */
) {
- PS_return(DBIpp_ph_qm) ? sprintf(start,"?") : sprintf(start,"%%s");
- dest = start + strlen(start);
- if (pln != idx) {
- char buf[99];
- sprintf(buf, "preparse found placeholder :%d out of sequence, expected :%d", pln, idx);
- set_err_char(dbh, imp_xxh, "1", 1, buf, 0, "preparse");
- return &PL_sv_undef;
- }
- while(isDIGIT(*src)) src++;
- idx++;
+ PS_return(DBIpp_ph_qm) ? sprintf(start,"?") : sprintf(start,"%%s");
+ dest = start + strlen(start);
+ if (pln != idx) {
+ char buf[99];
+ sprintf(buf, "preparse found placeholder :%d out of sequence, expected :%d", pln, idx);
+ set_err_char(dbh, imp_xxh, "1", 1, buf, 0, "preparse");
+ return &PL_sv_undef;
+ }
+ while(isDIGIT(*src)) src++;
+ idx++;
}
}
else if (isALNUM(*src)) /* :name */
diff --git a/t/60preparse.t b/t/60preparse.t
index 6432feb..668dfa3 100755
--- a/t/60preparse.t
+++ b/t/60preparse.t
@@ -11,7 +11,7 @@ BEGIN {
plan skip_all => 'preparse not supported for DBI::PurePerl';
}
else {
- plan tests => 39;
+ plan tests => 43;
}
}
@@ -71,6 +71,17 @@ is( pp($dbh, "a = :name", DBIpp_ph_sp, DBIpp_ph_cs), "a = %s" );
is( pp($dbh, "a = ? b = ? c = ?", DBIpp_ph_cn, DBIpp_ph_XX), "a = :p1 b = :p2 c = :p3" );
+my $max_placeholders = pp(
+ $dbh, "?" x 99999, DBIpp_ph_cn, DBIpp_ph_qm
+);
+ok(defined $max_placeholders, "accepts the documented limit of 99999 placeholders");
+is(substr($max_placeholders, -7), ":p99999", "numbers the final allowed placeholder");
+
+is(pp($dbh, "?" x 100000, DBIpp_ph_cn, DBIpp_ph_qm), undef,
+ "rejects placeholders above the documented limit");
+is($DBI::errstr, "preparse found more than 99999 '?' placeholders. Limit exceeded.",
+ "reports the placeholder limit");
+
## Placeholders inside comments (should be ignored where comments style is accepted):
is( pp( $dbh,
@@ -17,6 +17,7 @@ SRC_URI = "http://search.cpan.org/CPAN/authors/id/T/TI/TIMB/DBI-${PV}.tar.gz \
file://CVE-2026-14380_p2.patch \
file://CVE-2026-14380_p3.patch \
file://CVE-2026-14380_p4.patch \
file://CVE-2026-14739.patch \
"
SRC_URI[md5sum] = "352f80b1e23769c116082a90905d7398"
SRC_URI[sha256sum] = "8a2b993db560a2c373c174ee976a51027dd780ec766ae17620c20393d2e836fa"