Skip to content

Commit 5bb3e23

Browse files
tobil4skcmb69
authored andcommitted
Implement #77726: Allow null character in regex patterns
In 8b3c1a3, this was disallowed to fix #55856, which was a security issue caused by the /e modifier. The fix that was made was the "Easier fix" as described in the original report. With this fix, pattern strings are no longer treated as null terminated, so null characters can be placed inside and matched against with regex patterns without security problems, so there is no longer a reason to give the error. Allowing this is consistent with the behaviour of many other languages, including JavaScript, and thanks to PCRE2[0], it does not require manually escaping null characters. Now that we can avoid the error here without the cost of escaping characters, there is really no need anymore to stray here from the conventional behaviour. Currently, null characters are still disallowed before the first delimiter and in the options section at the end of a regex string, but these error messages have been updated. [0] Since PCRE2, pattern strings no longer have to be null terminated, and raw null characters match as normal. Closes GH-8114.
1 parent 98a4ab2 commit 5bb3e23

14 files changed

+170
-57
lines changed

NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ GD:
2424
- ODBC:
2525
. Fixed handling of single-key connection strings. (Calvin Buckley)
2626

27+
- PCRE:
28+
. Implemented FR #77726 (Allow null character in regex patterns). (cmb)
29+
2730
- PDO_ODBC:
2831
. Fixed handling of single-key connection strings. (Calvin Buckley)
2932

UPGRADING

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ PHP 8.2 UPGRADE NOTES
221221
- OCI8:
222222
. The minimum Oracle Client library version required is now 11.2.
223223

224+
- PCRE:
225+
. NUL characters (\0) in pattern strings are now supported.
226+
224227
- SQLite3:
225228
. sqlite3.defensive is now PHP_INI_USER.
226229

ext/pcre/php_pcre.c

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, in
624624
pcre_cache_entry new_entry;
625625
int rc;
626626
zend_string *key;
627-
pcre_cache_entry *ret;
627+
pcre_cache_entry *ret;
628628

629629
if (locale_aware && BG(ctype_string)) {
630630
key = zend_string_concat2(
@@ -645,28 +645,28 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, in
645645
}
646646

647647
p = ZSTR_VAL(regex);
648+
const char* end_p = ZSTR_VAL(regex) + ZSTR_LEN(regex);
648649

649650
/* Parse through the leading whitespace, and display a warning if we
650651
get to the end without encountering a delimiter. */
651652
while (isspace((int)*(unsigned char *)p)) p++;
652-
if (*p == 0) {
653+
if (p >= end_p) {
653654
if (key != regex) {
654655
zend_string_release_ex(key, 0);
655656
}
656-
php_error_docref(NULL, E_WARNING,
657-
p < ZSTR_VAL(regex) + ZSTR_LEN(regex) ? "Null byte in regex" : "Empty regular expression");
657+
php_error_docref(NULL, E_WARNING, "Empty regular expression");
658658
pcre_handle_exec_error(PCRE2_ERROR_INTERNAL);
659659
return NULL;
660660
}
661661

662662
/* Get the delimiter and display a warning if it is alphanumeric
663663
or a backslash. */
664664
delimiter = *p++;
665-
if (isalnum((int)*(unsigned char *)&delimiter) || delimiter == '\\') {
665+
if (isalnum((int)*(unsigned char *)&delimiter) || delimiter == '\\' || delimiter == '\0') {
666666
if (key != regex) {
667667
zend_string_release_ex(key, 0);
668668
}
669-
php_error_docref(NULL,E_WARNING, "Delimiter must not be alphanumeric or backslash");
669+
php_error_docref(NULL, E_WARNING, "Delimiter must not be alphanumeric, backslash, or NUL");
670670
pcre_handle_exec_error(PCRE2_ERROR_INTERNAL);
671671
return NULL;
672672
}
@@ -682,8 +682,8 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, in
682682
/* We need to iterate through the pattern, searching for the ending delimiter,
683683
but skipping the backslashed delimiters. If the ending delimiter is not
684684
found, display a warning. */
685-
while (*pp != 0) {
686-
if (*pp == '\\' && pp[1] != 0) pp++;
685+
while (pp < end_p) {
686+
if (*pp == '\\' && pp + 1 < end_p) pp++;
687687
else if (*pp == delimiter)
688688
break;
689689
pp++;
@@ -695,8 +695,8 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, in
695695
* reach the end of the pattern without matching, display a warning.
696696
*/
697697
int brackets = 1; /* brackets nesting level */
698-
while (*pp != 0) {
699-
if (*pp == '\\' && pp[1] != 0) pp++;
698+
while (pp < end_p) {
699+
if (*pp == '\\' && pp + 1 < end_p) pp++;
700700
else if (*pp == end_delimiter && --brackets <= 0)
701701
break;
702702
else if (*pp == start_delimiter)
@@ -705,13 +705,11 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, in
705705
}
706706
}
707707

708-
if (*pp == 0) {
708+
if (pp >= end_p) {
709709
if (key != regex) {
710710
zend_string_release_ex(key, 0);
711711
}
712-
if (pp < ZSTR_VAL(regex) + ZSTR_LEN(regex)) {
713-
php_error_docref(NULL,E_WARNING, "Null byte in regex");
714-
} else if (start_delimiter == end_delimiter) {
712+
if (start_delimiter == end_delimiter) {
715713
php_error_docref(NULL,E_WARNING, "No ending delimiter '%c' found", delimiter);
716714
} else {
717715
php_error_docref(NULL,E_WARNING, "No ending matching delimiter '%c' found", delimiter);
@@ -729,7 +727,7 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, in
729727

730728
/* Parse through the options, setting appropriate flags. Display
731729
a warning if we encounter an unknown modifier. */
732-
while (pp < ZSTR_VAL(regex) + ZSTR_LEN(regex)) {
730+
while (pp < end_p) {
733731
switch (*pp++) {
734732
/* Perl compatible options */
735733
case 'i': coptions |= PCRE2_CASELESS; break;
@@ -764,9 +762,9 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, in
764762

765763
default:
766764
if (pp[-1]) {
767-
php_error_docref(NULL,E_WARNING, "Unknown modifier '%c'", pp[-1]);
765+
php_error_docref(NULL, E_WARNING, "Unknown modifier '%c'", pp[-1]);
768766
} else {
769-
php_error_docref(NULL,E_WARNING, "Null byte in regex");
767+
php_error_docref(NULL, E_WARNING, "NUL is not a valid modifier");
770768
}
771769
pcre_handle_exec_error(PCRE2_ERROR_INTERNAL);
772770
efree(pattern);
@@ -2438,12 +2436,6 @@ PHP_FUNCTION(preg_replace_callback_array)
24382436
}
24392437

24402438
ZEND_HASH_FOREACH_STR_KEY_VAL(pattern, str_idx_regex, replace) {
2441-
if (!str_idx_regex) {
2442-
php_error_docref(NULL, E_WARNING, "Delimiter must not be alphanumeric or backslash");
2443-
RETVAL_NULL();
2444-
goto error;
2445-
}
2446-
24472439
if (!zend_is_callable_ex(replace, NULL, 0, NULL, &fcc, NULL)) {
24482440
zend_argument_type_error(1, "must contain only valid callbacks");
24492441
goto error;

ext/pcre/tests/bug73392.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ var_dump(preg_replace_callback_array(
2121
), 'a'));
2222
?>
2323
--EXPECTF--
24-
Warning: preg_replace_callback_array(): Delimiter must not be alphanumeric or backslash in %sbug73392.php on line %d
24+
Warning: preg_replace_callback_array(): Delimiter must not be alphanumeric, backslash, or NUL in %sbug73392.php on line %d
2525
NULL

ext/pcre/tests/delimiters.phpt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var_dump(preg_match('~a', ''));
1212
var_dump(preg_match('@\@\@@', '@@'));
1313
var_dump(preg_match('//z', '@@'));
1414
var_dump(preg_match('{', ''));
15+
var_dump(preg_match("\0\0", ''));
1516

1617
?>
1718
--EXPECTF--
@@ -22,7 +23,7 @@ Warning: preg_match(): Empty regular expression in %sdelimiters.php on line 4
2223
bool(false)
2324
int(1)
2425

25-
Warning: preg_match(): Delimiter must not be alphanumeric or backslash in %sdelimiters.php on line 6
26+
Warning: preg_match(): Delimiter must not be alphanumeric, backslash, or NUL in %sdelimiters.php on line 6
2627
bool(false)
2728
int(1)
2829

@@ -35,3 +36,6 @@ bool(false)
3536

3637
Warning: preg_match(): No ending matching delimiter '}' found in %sdelimiters.php on line 11
3738
bool(false)
39+
40+
Warning: preg_match(): Delimiter must not be alphanumeric, backslash, or NUL in %sdelimiters.php on line 12
41+
bool(false)

ext/pcre/tests/null_bytes.phpt

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,64 @@ Zero byte test
33
--FILE--
44
<?php
55

6-
preg_match("\0//i", "");
7-
preg_match("/\0/i", "");
8-
preg_match("//\0i", "");
9-
preg_match("//i\0", "");
10-
preg_match("/\\\0/i", "");
11-
12-
preg_match("\0[]i", "");
13-
preg_match("[\0]i", "");
14-
preg_match("[]\0i", "");
15-
preg_match("[]i\0", "");
16-
preg_match("[\\\0]i", "");
6+
var_dump(preg_match("\0//i", ""));
7+
var_dump(preg_match("/\0/i", ""));
8+
var_dump(preg_match("/\0/i", "\0"));
9+
var_dump(preg_match("//\0i", ""));
10+
var_dump(preg_match("//i\0", ""));
11+
var_dump(preg_match("/\\\0/i", ""));
12+
var_dump(preg_match("/\\\0/i", "\\\0"));
1713

18-
preg_replace("/foo/e\0/i", "echo('Eek');", "");
19-
20-
?>
21-
--EXPECTF--
22-
Warning: preg_match(): Null byte in regex in %snull_bytes.php on line 3
14+
var_dump(preg_match("\0[]i", ""));
15+
var_dump(preg_match("[\0]i", ""));
16+
var_dump(preg_match("[\0]i", "\0"));
17+
var_dump(preg_match("[]\0i", ""));
18+
var_dump(preg_match("[]i\0", ""));
19+
var_dump(preg_match("[\\\0]i", ""));
20+
var_dump(preg_match("[\\\0]i", "\\\0"));
2321

24-
Warning: preg_match(): Null byte in regex in %snull_bytes.php on line 4
22+
var_dump(preg_match("/abc\0def/", "abc"));
23+
var_dump(preg_match("/abc\0def/", "abc\0def"));
24+
var_dump(preg_match("/abc\0def/", "abc\0fed"));
2525

26-
Warning: preg_match(): Null byte in regex in %snull_bytes.php on line 5
26+
var_dump(preg_match("[abc\0def]", "abc"));
27+
var_dump(preg_match("[abc\0def]", "abc\0def"));
28+
var_dump(preg_match("[abc\0def]", "abc\0fed"));
2729

28-
Warning: preg_match(): Null byte in regex in %snull_bytes.php on line 6
30+
preg_replace("/foo/e\0/i", "echo('Eek');", "");
2931

30-
Warning: preg_match(): Null byte in regex in %snull_bytes.php on line 7
32+
?>
33+
--EXPECTF--
34+
Warning: preg_match(): Delimiter must not be alphanumeric, backslash, or NUL in %snull_bytes.php on line 3
35+
bool(false)
36+
int(0)
37+
int(1)
3138

32-
Warning: preg_match(): Null byte in regex in %snull_bytes.php on line 9
39+
Warning: preg_match(): NUL is not a valid modifier in %snull_bytes.php on line 6
40+
bool(false)
3341

34-
Warning: preg_match(): Null byte in regex in %snull_bytes.php on line 10
42+
Warning: preg_match(): NUL is not a valid modifier in %snull_bytes.php on line 7
43+
bool(false)
44+
int(0)
45+
int(1)
3546

36-
Warning: preg_match(): Null byte in regex in %snull_bytes.php on line 11
47+
Warning: preg_match(): Delimiter must not be alphanumeric, backslash, or NUL in %snull_bytes.php on line 11
48+
bool(false)
49+
int(0)
50+
int(1)
3751

38-
Warning: preg_match(): Null byte in regex in %snull_bytes.php on line 12
52+
Warning: preg_match(): NUL is not a valid modifier in %snull_bytes.php on line 14
53+
bool(false)
3954

40-
Warning: preg_match(): Null byte in regex in %snull_bytes.php on line 13
55+
Warning: preg_match(): NUL is not a valid modifier in %snull_bytes.php on line 15
56+
bool(false)
57+
int(0)
58+
int(1)
59+
int(0)
60+
int(1)
61+
int(0)
62+
int(0)
63+
int(1)
64+
int(0)
4165

42-
Warning: preg_replace(): Null byte in regex in %snull_bytes.php on line 15
66+
Warning: preg_replace(): NUL is not a valid modifier in %snull_bytes.php on line 27

ext/pcre/tests/preg_grep_error1.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ echo "Done"
3737

3838
Arg value is abcdef
3939

40-
Warning: preg_grep(): Delimiter must not be alphanumeric or backslash in %spreg_grep_error1.php on line %d
40+
Warning: preg_grep(): Delimiter must not be alphanumeric, backslash, or NUL in %spreg_grep_error1.php on line %d
4141
bool(false)
4242

4343
Arg value is /[a-zA-Z]

ext/pcre/tests/preg_match_all_error1.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ var_dump($matches);
3838

3939
Arg value is abcdef
4040

41-
Warning: preg_match_all(): Delimiter must not be alphanumeric or backslash in %spreg_match_all_error1.php on line %d
41+
Warning: preg_match_all(): Delimiter must not be alphanumeric, backslash, or NUL in %spreg_match_all_error1.php on line %d
4242
bool(false)
4343
NULL
4444

ext/pcre/tests/preg_match_error1.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ try {
3434

3535
Arg value is abcdef
3636

37-
Warning: preg_match(): Delimiter must not be alphanumeric or backslash in %spreg_match_error1.php on line %d
37+
Warning: preg_match(): Delimiter must not be alphanumeric, backslash, or NUL in %spreg_match_error1.php on line %d
3838
bool(false)
3939

4040
Arg value is /[a-zA-Z]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
--TEST--
2+
preg_replace_callback_array() errors
3+
--FILE--
4+
<?php
5+
6+
function b() {
7+
return "b";
8+
}
9+
10+
// empty strings
11+
12+
var_dump(preg_replace_callback_array(
13+
array(
14+
"/a/" => 'b',
15+
"" => function () { return "ok"; }), 'a'));
16+
17+
var_dump(preg_replace_callback_array(
18+
array(
19+
"/a/" => 'b',
20+
null => function () { return "ok"; }), 'a'));
21+
22+
// backslashes
23+
24+
var_dump(preg_replace_callback_array(
25+
array(
26+
"/a/" => 'b',
27+
"\\b\\" => function () { return "ok"; }), 'a'));
28+
29+
// alphanumeric delimiters
30+
31+
var_dump(preg_replace_callback_array(
32+
array(
33+
"/a/" => 'b',
34+
"aba" => function () { return "ok"; }), 'a'));
35+
36+
var_dump(preg_replace_callback_array(
37+
array(
38+
"/a/" => 'b',
39+
"1b1" => function () { return "ok"; }), 'a'));
40+
41+
// null character delimiter
42+
43+
var_dump(preg_replace_callback_array(
44+
array(
45+
"/a/" => 'b',
46+
"\0b\0" => function () { return "ok"; }), 'a'));
47+
48+
?>
49+
--EXPECTF--
50+
Warning: preg_replace_callback_array(): Empty regular expression in %spreg_replace_callback_array_error.php on line 12
51+
NULL
52+
53+
Warning: preg_replace_callback_array(): Empty regular expression in %spreg_replace_callback_array_error.php on line 17
54+
NULL
55+
56+
Warning: preg_replace_callback_array(): Delimiter must not be alphanumeric, backslash, or NUL in %spreg_replace_callback_array_error.php on line 24
57+
NULL
58+
59+
Warning: preg_replace_callback_array(): Delimiter must not be alphanumeric, backslash, or NUL in %spreg_replace_callback_array_error.php on line 31
60+
NULL
61+
62+
Warning: preg_replace_callback_array(): Delimiter must not be alphanumeric, backslash, or NUL in %spreg_replace_callback_array_error.php on line 36
63+
NULL
64+
65+
Warning: preg_replace_callback_array(): Delimiter must not be alphanumeric, backslash, or NUL in %spreg_replace_callback_array_error.php on line 43
66+
NULL
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
preg_replace_callback_array() invalid callable
3+
--FILE--
4+
<?php
5+
6+
function b() {
7+
return "b";
8+
}
9+
10+
// invalid callable
11+
var_dump(preg_replace_callback_array(
12+
array(
13+
"/a/" => 'b',
14+
"/b/" => 'invalid callable'), 'a'));
15+
16+
--EXPECTF--
17+
Fatal error: Uncaught TypeError: preg_replace_callback_array(): Argument #1 ($pattern) must contain only valid callbacks in %spreg_replace_callback_array_fatal_error.php:11
18+
Stack trace:
19+
#0 %spreg_replace_callback_array_fatal_error.php(11): preg_replace_callback_array(Array, 'a')
20+
#1 {main}
21+
thrown in %spreg_replace_callback_array_fatal_error.php on line 11

ext/pcre/tests/preg_replace_callback_error1.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ foreach($regex_array as $regex_value) {
3030

3131
Arg value is abcdef
3232

33-
Warning: preg_replace_callback(): Delimiter must not be alphanumeric or backslash in %s on line %d
33+
Warning: preg_replace_callback(): Delimiter must not be alphanumeric, backslash, or NUL in %s on line %d
3434
NULL
3535

3636
Arg value is /[a-zA-Z]

ext/pcre/tests/preg_replace_error1.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ try {
3333

3434
Arg value is abcdef
3535

36-
Warning: preg_replace(): Delimiter must not be alphanumeric or backslash in %spreg_replace_error1.php on line %d
36+
Warning: preg_replace(): Delimiter must not be alphanumeric, backslash, or NUL in %spreg_replace_error1.php on line %d
3737
NULL
3838

3939
Arg value is /[a-zA-Z]

0 commit comments

Comments
 (0)