Skip to content

Commit 3999b57

Browse files
Reject "name=value" INI settings whose value contains a line-break character
1 parent 18d46b5 commit 3999b57

2 files changed

Lines changed: 50 additions & 5 deletions

File tree

src/Util/PHP/JobRunner.php

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use function is_resource;
2929
use function proc_close;
3030
use function proc_open;
31+
use function sprintf;
3132
use function str_contains;
3233
use function str_replace;
3334
use function str_starts_with;
@@ -321,6 +322,8 @@ private function cliIniOverrides(array $alreadySet): array
321322
/**
322323
* @param list<string> $settings
323324
*
325+
* @throws PhpProcessException
326+
*
324327
* @return list<string>
325328
*/
326329
private function settingsToParameters(array $settings): array
@@ -329,23 +332,31 @@ private function settingsToParameters(array $settings): array
329332

330333
foreach ($settings as $setting) {
331334
$buffer[] = '-d';
332-
$buffer[] = $this->quoteSettingValue($setting);
335+
$buffer[] = $this->processSettingValue($setting);
333336
}
334337

335338
return $buffer;
336339
}
337340

338341
/**
339-
* Quotes the value portion of a "name=value" INI setting only when it
340-
* contains characters PHP's INI parser would otherwise interpret as
341-
* metacharacters (`;` starts a comment, `"` is a string delimiter).
342+
* Rejects "name=value" INI settings whose value contains a line-break
343+
* character. A newline cannot legitimately appear in a PHP INI value and
344+
* would, if forwarded unchanged, be parsed by the child process as a
345+
* directive separator — turning a single setting into an attacker-
346+
* controlled sequence of directives.
347+
*
348+
* Otherwise quotes the value portion only when it contains characters
349+
* PHP's INI parser would interpret as metacharacters (`;` starts a
350+
* comment, `"` is a string delimiter).
342351
*
343352
* Quoting is avoided for plain values so that boolean keywords such as
344353
* `On` / `Off` keep their special INI semantics; wrapping them in quotes
345354
* turns them into the literal strings `"On"` / `"Off"` and breaks
346355
* settings like `output_buffering`.
356+
*
357+
* @throws PhpProcessException
347358
*/
348-
private function quoteSettingValue(string $setting): string
359+
private function processSettingValue(string $setting): string
349360
{
350361
$parts = explode('=', $setting, 2);
351362

@@ -355,6 +366,15 @@ private function quoteSettingValue(string $setting): string
355366

356367
[$name, $value] = $parts;
357368

369+
if (str_contains($value, "\n") || str_contains($value, "\r")) {
370+
throw new PhpProcessException(
371+
sprintf(
372+
'PHP setting "%s" contains a line-break character, which is not permitted',
373+
$name,
374+
),
375+
);
376+
}
377+
358378
if (!str_contains($value, ';') && !str_contains($value, '"')) {
359379
return $setting;
360380
}

tests/unit/Util/PHP/JobRunnerTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,29 @@ public function testRunsJobInSeparateProcess(Result $expected, Job $job): void
155155
$this->assertSame($expected->stdout(), $result->stdout());
156156
$this->assertSame($expected->stderr(), $result->stderr());
157157
}
158+
159+
public function testRejectsPhpSettingValueContainingLineBreak(): void
160+
{
161+
$jobRunner = new JobRunner(
162+
new ChildProcessResultProcessor(
163+
new Facade,
164+
$this->createStub(Emitter::class),
165+
new PassedTests,
166+
new CodeCoverage,
167+
),
168+
);
169+
170+
$job = new Job(
171+
<<<'EOT'
172+
<?php declare(strict_types=1);
173+
174+
EOT,
175+
phpSettings: ["highlight.string=foo\nauto_prepend_file=/tmp/evil.php"],
176+
);
177+
178+
$this->expectException(PhpProcessException::class);
179+
$this->expectExceptionMessage('PHP setting "highlight.string" contains a line-break character, which is not permitted');
180+
181+
$jobRunner->run($job);
182+
}
158183
}

0 commit comments

Comments
 (0)