From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 267761FF0AF for ; Thu, 08 Oct 2026 15:52:53 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 079DE21666; Thu, 08 Oct 2026 15:52:30 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Subject: [PATCH common 3/3] fix #7612: json schema: don't allow integers that are too large Date: Thu, 8 Oct 2026 15:48:13 +0200 Message-ID: <20261008135224.3323985-4-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261008135224.3323985-1-d.csapak@proxmox.com> References: <20261008135224.3323985-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.374 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: VJDNXTZMAVGZ3ABINS7BLO66JQXCC5YR X-Message-ID-Hash: VJDNXTZMAVGZ3ABINS7BLO66JQXCC5YR X-MailFrom: d.csapak@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: very large integers (such as 1e400) are represented as 'inf' which are serialized out as such by the JSON module. Since that is invalid JSON, we want to catch these invalid values already during parsing, because +/- infinity rarely make sense when we expect a number. Improve the check by also converting the string to a number and back, then check this for 'inf/nan' variants via Scalar::Util::Numeric. This now let's some numeric test fail for huge numbers, while still allowing smaller ones and correctly identifying non-numeric strings. Signed-off-by: Dominik Csapak --- debian/control | 1 + src/PVE/JSONSchema.pm | 16 +++++++-- test/json-schema-test.pl | 76 ++++++++++++++++++++++++++++------------ 3 files changed, 69 insertions(+), 24 deletions(-) diff --git a/debian/control b/debian/control index ef9b0eb..3c78e55 100644 --- a/debian/control +++ b/debian/control @@ -48,6 +48,7 @@ Depends: libanyevent-perl, libnetaddr-ip-perl, libproxmox-acme-perl, libproxmox-rs-perl, + libscalar-util-numeric-perl, libstring-shellquote-perl, libtimedate-perl, liburi-perl, diff --git a/src/PVE/JSONSchema.pm b/src/PVE/JSONSchema.pm index db2c167..f06b282 100644 --- a/src/PVE/JSONSchema.pm +++ b/src/PVE/JSONSchema.pm @@ -11,6 +11,7 @@ use Getopt::Long; use HTTP::Status qw(:constants); use JSON; use Net::IP qw(:PROC); +use Scalar::Util::Numeric qw(isinf isnan); use Storable; # for dclone use PVE::Exception qw(raise); @@ -1162,17 +1163,28 @@ sub add_error { } } +# test values if they're in numeric finite range with the given REGEX +my sub numeric_test { + my ($value, $NUM_RE) = @_; + + my $res = $value =~ $NUM_RE; + return $res if !$res; + my $num = $value; + $num += 0; + return !isinf($num) && !isnan($num); +} + sub is_number { my $value = shift; # see 'man perlretut' - return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?\z/; + return numeric_test($value, qr/^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?\z/); } sub is_integer { my $value = shift; - return $value =~ m/^[+-]?\d+\z/; + return numeric_test($value, qr/^[+-]?\d+\z/); } sub check_type { diff --git a/test/json-schema-test.pl b/test/json-schema-test.pl index c5014e9..cda7c4d 100755 --- a/test/json-schema-test.pl +++ b/test/json-schema-test.pl @@ -112,26 +112,26 @@ my $property_string_tests = [ 'num-ranged' => { type => 'number', optional => 1, minimum => -1, maximum => 1 }, }, subtests => [ - { in => "int=$huge_int", out => { int => $huge_int } }, - { in => "int=-$huge_int", out => { int => "-$huge_int" } }, - { in => "num=$huge_int", out => { num => $huge_int } }, - { in => "num=1e400", out => { num => '1e400' } }, - { in => "num=-1e400", out => { num => '-1e400' } }, + { in => "int=$huge_int", must_fail => qr/type check \('integer'\) failed/ }, + { in => "int=-$huge_int", must_fail => qr/type check \('integer'\) failed/ }, + { in => "num=$huge_int", must_fail => qr/type check \('number'\) failed/ }, + { in => "num=1e400", must_fail => qr/type check \('number'\) failed/ }, + { in => "num=-1e400", must_fail => qr/type check \('number'\) failed/ }, { in => "int-ranged=$huge_int", - must_fail => qr/int-ranged: value must have a maximum value of 10/, + must_fail => qr/type check \('integer'\) failed/, }, { in => "int-ranged=-$huge_int", - must_fail => qr/int-ranged: value must have a minimum value of -10/, + must_fail => qr/type check \('integer'\) failed/, }, { in => "num-ranged=1e400", - must_fail => qr/num-ranged: value must have a maximum value of 1/, + must_fail => qr/type check \('number'\) failed/, }, { in => "num-ranged=-1e400", - must_fail => qr/num-ranged: value must have a minimum value of -1/, + must_fail => qr/type check \('number'\) failed/, }, ], }, @@ -933,9 +933,21 @@ my $numeric_tests = [ name => 'huge integer', schema => { type => 'integer' }, subtests => [ - { name => 'positive', in => $huge_int }, - { name => 'negative', in => "-$huge_int" }, - { name => 'explicit plus sign', in => "+$huge_int" }, + { + name => 'positive', + in => $huge_int, + must_fail => qr/^type check \('integer'\) failed/, + }, + { + name => 'negative', + in => "-$huge_int", + must_fail => qr/^type check \('integer'\) failed/, + }, + { + name => 'explicit plus sign', + in => "+$huge_int", + must_fail => qr/^type check \('integer'\) failed/, + }, { name => 'exponent notation', in => '1e400', @@ -955,12 +967,12 @@ my $numeric_tests = [ { name => 'above maximum', in => $huge_int, - must_fail => qr/^value must have a maximum value of 10$/, + must_fail => qr/^type check \('integer'\) failed/, }, { name => 'below minimum', in => "-$huge_int", - must_fail => qr/^value must have a minimum value of -10$/, + must_fail => qr/^type check \('integer'\) failed/, }, ], }, @@ -968,11 +980,31 @@ my $numeric_tests = [ name => 'huge number', schema => { type => 'number' }, subtests => [ - { name => 'integer notation', in => $huge_int }, - { name => 'negative integer notation', in => "-$huge_int" }, - { name => 'fractional part', in => "$huge_int.5" }, - { name => 'exponent notation', in => '1e400' }, - { name => 'negative exponent notation', in => '-1e400' }, + { + name => 'integer notation', + in => $huge_int, + must_fail => qr/^type check \('number'\) failed/, + }, + { + name => 'negative integer notation', + in => "-$huge_int", + must_fail => qr/^type check \('number'\) failed/, + }, + { + name => 'fractional part', + in => "$huge_int.5", + must_fail => qr/^type check \('number'\) failed/, + }, + { + name => 'exponent notation', + in => '1e400', + must_fail => qr/^type check \('number'\) failed/, + }, + { + name => 'negative exponent notation', + in => '-1e400', + must_fail => qr/^type check \('number'\) failed/, + }, { name => 'tiny exponent notation', in => '1e-400' }, { name => 'infinity literal', @@ -988,17 +1020,17 @@ my $numeric_tests = [ { name => 'above maximum', in => '1e400', - must_fail => qr/^value must have a maximum value of 1$/, + must_fail => qr/^type check \('number'\) failed/, }, { name => 'above maximum in integer notation', in => $huge_int, - must_fail => qr/^value must have a maximum value of 1$/, + must_fail => qr/^type check \('number'\) failed/, }, { name => 'below minimum', in => '-1e400', - must_fail => qr/^value must have a minimum value of 0$/, + must_fail => qr/^type check \('number'\) failed/, }, { name => 'tiny value within range', in => '1e-400' }, ], -- 2.47.3