Skip to content

Commit 60c68c3

Browse files
committed
Fix #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.
1 parent 8f5480e commit 60c68c3

File tree

3 files changed

+72
-44
lines changed

3 files changed

+72
-44
lines changed

ext/pcre/php_pcre.c

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, in
615615
char delimiter;
616616
char start_delimiter;
617617
char end_delimiter;
618-
char *p, *pp;
618+
char *p, *pp, *end_p;
619619
char *pattern;
620620
size_t pattern_len;
621621
uint32_t poptions = 0;
@@ -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,30 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, in
645645
}
646646

647647
p = ZSTR_VAL(regex);
648+
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 == '\0' ?
670+
"Delimiter must not be a null character" :
671+
"Delimiter must not be alphanumeric or backslash");
670672
pcre_handle_exec_error(PCRE2_ERROR_INTERNAL);
671673
return NULL;
672674
}
@@ -682,8 +684,8 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, in
682684
/* We need to iterate through the pattern, searching for the ending delimiter,
683685
but skipping the backslashed delimiters. If the ending delimiter is not
684686
found, display a warning. */
685-
while (*pp != 0) {
686-
if (*pp == '\\' && pp[1] != 0) pp++;
687+
while (pp < end_p) {
688+
if (*pp == '\\' && pp + 1 < end_p) pp++;
687689
else if (*pp == delimiter)
688690
break;
689691
pp++;
@@ -695,8 +697,8 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, in
695697
* reach the end of the pattern without matching, display a warning.
696698
*/
697699
int brackets = 1; /* brackets nesting level */
698-
while (*pp != 0) {
699-
if (*pp == '\\' && pp[1] != 0) pp++;
700+
while (pp < end_p) {
701+
if (*pp == '\\' && pp + 1 < end_p) pp++;
700702
else if (*pp == end_delimiter && --brackets <= 0)
701703
break;
702704
else if (*pp == start_delimiter)
@@ -705,13 +707,11 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, in
705707
}
706708
}
707709

708-
if (*pp == 0) {
710+
if (pp == end_p) {
709711
if (key != regex) {
710712
zend_string_release_ex(key, 0);
711713
}
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) {
714+
if (start_delimiter == end_delimiter) {
715715
php_error_docref(NULL,E_WARNING, "No ending delimiter '%c' found", delimiter);
716716
} else {
717717
php_error_docref(NULL,E_WARNING, "No ending matching delimiter '%c' found", delimiter);
@@ -729,7 +729,7 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, in
729729

730730
/* Parse through the options, setting appropriate flags. Display
731731
a warning if we encounter an unknown modifier. */
732-
while (pp < ZSTR_VAL(regex) + ZSTR_LEN(regex)) {
732+
while (pp < end_p) {
733733
switch (*pp++) {
734734
/* Perl compatible options */
735735
case 'i': coptions |= PCRE2_CASELESS; break;
@@ -763,9 +763,9 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, in
763763

764764
default:
765765
if (pp[-1]) {
766-
php_error_docref(NULL,E_WARNING, "Unknown modifier '%c'", pp[-1]);
766+
php_error_docref(NULL, E_WARNING, "Unknown modifier '%c'", pp[-1]);
767767
} else {
768-
php_error_docref(NULL,E_WARNING, "Null byte in regex");
768+
php_error_docref(NULL, E_WARNING, "Null character is not a valid modifier");
769769
}
770770
pcre_handle_exec_error(PCRE2_ERROR_INTERNAL);
771771
efree(pattern);

ext/pcre/tests/delimiters.phpt

Lines changed: 4 additions & 0 deletions
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--
@@ -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 a null character 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 a null character 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(): Null character 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(): Null character 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 a null character 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(): Null character 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(): Null character 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(): Null character is not a valid modifier in %snull_bytes.php on line 27

0 commit comments

Comments
 (0)