Skip to content

Commit c4f04e5

Browse files
committed
Fix submit-event.php crash on non-numeric date fields
The event submission form passed $_POST date/recurrence fields straight to checkdate() and mktime(). Under PHP 8 these require int, so any non-numeric string or array value (bots and scanners fuzzing the form) threw an uncaught TypeError -- 9,393 fatals in a 15-day window. Normalize the numeric fields to integers at the input boundary via a new testable phpweb\Events\EventInput::normalizeNumericFields(); non-numeric input becomes 0, an invalid date the existing validation already rejects. Add unit tests for the normalization and a regression test that the normalized values are safe to pass to checkdate().
1 parent 9ffddc4 commit c4f04e5

3 files changed

Lines changed: 116 additions & 9 deletions

File tree

src/Events/EventInput.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpweb\Events;
6+
7+
final class EventInput
8+
{
9+
/**
10+
* The numeric event fields (start/end date parts and recurrence) that must be
11+
* integers before being passed to checkdate() or mktime().
12+
*/
13+
private const NUMERIC_FIELDS = [
14+
'sday', 'smonth', 'syear', 'eday',
15+
'emonth', 'eyear', 'recur', 'recur_day',
16+
];
17+
18+
/**
19+
* Coerce the numeric event fields of a raw request array to integers.
20+
*
21+
* Untrusted input (non-numeric strings, arrays, missing values) would otherwise
22+
* reach checkdate()/mktime() and throw a TypeError under PHP 8. Anything that is
23+
* not a numeric value becomes 0, i.e. an invalid date that the normal form
24+
* validation already rejects. Non-numeric fields are left untouched.
25+
*
26+
* @param array<string, mixed> $post
27+
* @return array<string, mixed>
28+
*/
29+
public static function normalizeNumericFields(array $post): array
30+
{
31+
foreach (self::NUMERIC_FIELDS as $field) {
32+
$value = $post[$field] ?? null;
33+
$post[$field] = is_numeric($value) ? (int) $value : 0;
34+
}
35+
36+
return $post;
37+
}
38+
}

submit-event.php

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<?php
2+
use phpweb\Events\EventInput;
3+
24
$_SERVER['BASE_PAGE'] = 'submit-event.php';
35
include_once __DIR__ . '/include/prepend.inc';
46
include_once __DIR__ . '/include/posttohost.inc';
@@ -9,16 +11,11 @@
911
$errors = [];
1012
$process = [] !== $_POST;
1113

14+
// Coerce the numeric date/recurrence fields to integers so untrusted input can be
15+
// passed safely to checkdate()/mktime() below instead of throwing a TypeError.
16+
$_POST = EventInput::normalizeNumericFields($_POST);
17+
1218
// Avoid E_NOTICE errors on incoming vars if not set
13-
$vars = [
14-
'sday', 'smonth', 'syear', 'eday',
15-
'emonth', 'eyear', 'recur', 'recur_day',
16-
];
17-
foreach ($vars as $varname) {
18-
if (empty($_POST[$varname])) {
19-
$_POST[$varname] = 0;
20-
}
21-
}
2219
$vars = [
2320
'type', 'country', 'category', 'email', 'url', 'ldesc', 'sdesc',
2421
];
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpweb\Test\Unit\Events;
6+
7+
use phpweb\Events\EventInput;
8+
use PHPUnit\Framework;
9+
10+
#[Framework\Attributes\CoversClass(EventInput::class)]
11+
final class EventInputTest extends Framework\TestCase
12+
{
13+
public function testCoercesNumericStringsToIntegers(): void
14+
{
15+
$result = EventInput::normalizeNumericFields(['smonth' => '12', 'sday' => '25', 'syear' => '2026']);
16+
17+
self::assertSame(12, $result['smonth']);
18+
self::assertSame(25, $result['sday']);
19+
self::assertSame(2026, $result['syear']);
20+
}
21+
22+
public function testCoercesNonNumericStringsToZero(): void
23+
{
24+
$result = EventInput::normalizeNumericFields(['smonth' => 'January', 'sday' => 'abc', 'syear' => '12abc']);
25+
26+
self::assertSame(0, $result['smonth']);
27+
self::assertSame(0, $result['sday']);
28+
self::assertSame(0, $result['syear']);
29+
}
30+
31+
public function testCoercesArrayValuesToZero(): void
32+
{
33+
$result = EventInput::normalizeNumericFields(['smonth' => ['x'], 'sday' => [], 'syear' => ['1', '2']]);
34+
35+
self::assertSame(0, $result['smonth']);
36+
self::assertSame(0, $result['sday']);
37+
self::assertSame(0, $result['syear']);
38+
}
39+
40+
public function testDefaultsMissingFieldsToZero(): void
41+
{
42+
$result = EventInput::normalizeNumericFields([]);
43+
44+
foreach (['sday', 'smonth', 'syear', 'eday', 'emonth', 'eyear', 'recur', 'recur_day'] as $field) {
45+
self::assertSame(0, $result[$field], $field);
46+
}
47+
}
48+
49+
public function testLeavesNonNumericFieldsUntouched(): void
50+
{
51+
$result = EventInput::normalizeNumericFields(['email' => 'a@b.com', 'type' => 'multi', 'smonth' => '5']);
52+
53+
self::assertSame('a@b.com', $result['email']);
54+
self::assertSame('multi', $result['type']);
55+
self::assertSame(5, $result['smonth']);
56+
}
57+
58+
/**
59+
* Regression test for the production fatal:
60+
* Uncaught TypeError: checkdate(): Argument #1 ($month) must be of type int, string given
61+
*
62+
* After normalization the date parts are always integers, so passing them to
63+
* checkdate() (and mktime()) can no longer throw a TypeError; garbage input
64+
* simply becomes an invalid (0) date that the normal validation rejects.
65+
*/
66+
public function testNormalizedValuesAreSafeForCheckdate(): void
67+
{
68+
$result = EventInput::normalizeNumericFields(['smonth' => 'notamonth', 'sday' => '1', 'syear' => '2026']);
69+
70+
self::assertFalse(checkdate($result['smonth'], $result['sday'], $result['syear']));
71+
}
72+
}

0 commit comments

Comments
 (0)