fix: return "Invalid data." instead of HTTP 500 on malformed v2 payloads

Under strict_types on PHP 8, malformed JSON API v2 payloads triggered
uncaught TypeErrors (HTTP 500) instead of the standard JSON error
response {"status":1,"message":"Invalid data."}:

- a non-numeric `v` reached `$data['v'] + 0` in Request::getData()
- a scalar `meta` reached `count($message['meta'])` in FormatV2::isValid()
- non-array adata[0] / non-string ct fields reached base64_decode(),
  strlen() and count() with the wrong types

Validate the types before the numeric coercion and before the
string/count operations, so all such inputs are rejected cleanly with
"Invalid data." instead of a fatal error. Both crashes are remotely
triggerable without authentication (availability only, no data exposure).

Adds FormatV2Test coverage for the malformed-type inputs.

Fixes #1883
This commit is contained in:
marouanehassine 2026-07-17 14:44:04 +02:00
parent 886d4419d7
commit e80efda7c2
3 changed files with 60 additions and 3 deletions

View file

@ -56,7 +56,21 @@ class FormatV2
return false;
}
$cipherParams = $isComment ? $message['adata'] : $message['adata'][0];
$cipherParams = $isComment ? $message['adata'] : ($message['adata'][0] ?? null);
// Make sure the cipher parameters are a properly sized array.
if (!is_array($cipherParams) || count($cipherParams) < 8) {
return false;
}
// Make sure the ciphertext and the cipher parameters used in the
// string operations below are actually strings, so that malformed
// input yields "Invalid data." instead of a fatal type error.
if (!is_string($message['ct']) ||
!is_string($cipherParams[0] ?? null) ||
!is_string($cipherParams[1] ?? null)) {
return false;
}
// Make sure some fields are base64 data:
// - initialization vector
@ -119,6 +133,7 @@ class FormatV2
// require only the key 'expire' in the metadata of pastes
if (!$isComment && (
!is_array($message['meta']) ||
count($message['meta']) === 0 ||
!array_key_exists('expire', $message['meta']) ||
count($message['meta']) > 1

View file

@ -189,8 +189,13 @@ class Request
foreach ($required_keys as $key) {
$data[$key] = $this->getParam($key, $key === 'v' ? 1 : '');
}
// forcing a cast to int or float
$data['v'] = $data['v'] + 0;
// forcing a cast to int or float, but only when the value is
// numeric; a non-numeric version is left untouched so the format
// validator rejects it with "Invalid data." instead of a fatal
// type error
if (is_numeric($data['v'])) {
$data['v'] = $data['v'] + 0;
}
return $data;
}

View file

@ -72,4 +72,41 @@ class FormatV2Test extends TestCase
$paste = Helper::getPaste();
$this->assertFalse(FormatV2::isValid($paste), 'invalid meta key');
}
public function testFormatV2ValidatorRejectsMalformedTypes()
{
// malformed input must yield "false" (Invalid data) rather than a
// fatal type error under strict_types
$paste = Helper::getPastePost();
$paste['v'] = 'abc';
$this->assertFalse(FormatV2::isValid($paste), 'non-numeric version');
$paste = Helper::getPastePost();
$paste['v'] = array();
$this->assertFalse(FormatV2::isValid($paste), 'array version');
$paste = Helper::getPastePost();
$paste['ct'] = array('not', 'a', 'string');
$this->assertFalse(FormatV2::isValid($paste), 'non-string ciphertext');
$paste = Helper::getPastePost();
$paste['meta'] = 'not-an-array';
$this->assertFalse(FormatV2::isValid($paste), 'non-array meta');
$paste = Helper::getPastePost();
$paste['adata'][0] = 'not-an-array';
$this->assertFalse(FormatV2::isValid($paste), 'non-array cipher parameters');
$paste = Helper::getPastePost();
$paste['adata'][0] = array();
$this->assertFalse(FormatV2::isValid($paste), 'empty cipher parameters');
$paste = Helper::getPastePost();
$paste['adata'][0][0] = array();
$this->assertFalse(FormatV2::isValid($paste), 'non-string iv');
$comment = Helper::getCommentPost();
$comment['ct'] = 42;
$this->assertFalse(FormatV2::isValid($comment, true), 'non-string comment ciphertext');
}
}