diff --git a/src/Util/PHP/JobRunner.php b/src/Util/PHP/JobRunner.php index c70147bb0ba..7ec61d506af 100644 --- a/src/Util/PHP/JobRunner.php +++ b/src/Util/PHP/JobRunner.php @@ -26,8 +26,12 @@ use function is_resource; use function proc_close; use function proc_open; +use function str_contains; +use function str_replace; use function str_starts_with; use function stream_get_contents; +use function strpos; +use function substr; use function sys_get_temp_dir; use function tempnam; use function trim; @@ -325,9 +329,38 @@ private function settingsToParameters(array $settings): array foreach ($settings as $setting) { $buffer[] = '-d'; - $buffer[] = $setting; + $buffer[] = $this->quoteSettingValue($setting); } return $buffer; } + + /** + * Quotes the value portion of a "name=value" INI setting only when it + * contains characters PHP's INI parser would otherwise interpret as + * metacharacters (`;` starts a comment, `"` is a string delimiter). + * + * Quoting is avoided for plain values so that boolean keywords such as + * `On` / `Off` keep their special INI semantics; wrapping them in quotes + * turns them into the literal strings `"On"` / `"Off"` and breaks + * settings like `output_buffering`. + */ + private function quoteSettingValue(string $setting): string + { + $position = strpos($setting, '='); + + if ($position === false) { + return $setting; + } + + $value = substr($setting, $position + 1); + + if (!str_contains($value, ';') && !str_contains($value, '"')) { + return $setting; + } + + $name = substr($setting, 0, $position); + + return $name . '="' . str_replace('"', '\\"', $value) . '"'; + } } diff --git a/tests/unit/Util/PHP/JobRunnerTest.php b/tests/unit/Util/PHP/JobRunnerTest.php index 53c225011b7..17e5083455e 100644 --- a/tests/unit/Util/PHP/JobRunnerTest.php +++ b/tests/unit/Util/PHP/JobRunnerTest.php @@ -110,6 +110,20 @@ public static function provider(): Generator input: 'test', ), ]; + + $obfuscationRegex = '(?i)(?:(?:"|%22)?)(?:(?:old[-_]?|new[-_]?)?p(?:ass)?w(?:or)?d(?:1|2)?|pass(?:[-_]?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:(?:\s|%20)*(?:=|%3D)[^&]+|(?:"|%22)(?:\s|%20)*(?::|%3A)(?:\s|%20)*(?:"|%22)(?:%2[^2]|%[^2]|[^"%])+(?:"|%22))|bearer(?:\s|%20)+[a-z0-9\._\-]+|token(?::|%3A)[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L](?:[\w=-]|%3D)+\.ey[I-L](?:[\w=-]|%3D)+(?:\.(?:[\w.+\/=-]|%3D|%2F|%2B)+)?|-{5}BEGIN(?:[a-z\s]|%20)+PRIVATE(?:\s|%20)KEY-{5}[^\-]+-{5}END(?:[a-z\s]|%20)+PRIVATE(?:\s|%20)KEY(?:-{5})?(?:\n|%0A)?'; + + yield 'php setting value containing INI metacharacters' => [ + new Result($obfuscationRegex, ''), + new Job( + <<<'EOT' +