feat: validate script mapping paths
This commit is contained in:
parent
bca325cc07
commit
4dc27cb86f
2 changed files with 92 additions and 0 deletions
44
src/Service/PathNormalizer.php
Normal file
44
src/Service/PathNormalizer.php
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
final class PathNormalizer
|
||||||
|
{
|
||||||
|
public function publicPath(string $path): string
|
||||||
|
{
|
||||||
|
$normalized = $this->relativePath($path);
|
||||||
|
|
||||||
|
if (!str_ends_with($normalized, '.sh')) {
|
||||||
|
throw new InvalidArgumentException('Public path must end with .sh.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function repositoryPath(string $path): string
|
||||||
|
{
|
||||||
|
return $this->relativePath($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function relativePath(string $path): string
|
||||||
|
{
|
||||||
|
$path = trim(str_replace('\\', '/', $path));
|
||||||
|
$path = ltrim($path, '/');
|
||||||
|
|
||||||
|
if ($path === '') {
|
||||||
|
throw new InvalidArgumentException('Path cannot be empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$segments = explode('/', $path);
|
||||||
|
|
||||||
|
foreach ($segments as $segment) {
|
||||||
|
if ($segment === '' || $segment === '.' || $segment === '..') {
|
||||||
|
throw new InvalidArgumentException('Path contains an invalid segment.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode('/', $segments);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
tests/Service/PathNormalizerTest.php
Normal file
48
tests/Service/PathNormalizerTest.php
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Service\PathNormalizer;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class PathNormalizerTest extends TestCase
|
||||||
|
{
|
||||||
|
private PathNormalizer $normalizer;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->normalizer = new PathNormalizer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPublicPathIsNormalized(): void
|
||||||
|
{
|
||||||
|
self::assertSame('mcp/graylog/install.sh', $this->normalizer->publicPath('/mcp/graylog/install.sh'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPublicPathMustEndWithShellExtension(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
|
||||||
|
$this->normalizer->publicPath('mcp/graylog/install.txt');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPublicPathRejectsTraversal(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
|
||||||
|
$this->normalizer->publicPath('mcp/../install.sh');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRepositoryPathAllowsNestedFileWithoutShellRequirement(): void
|
||||||
|
{
|
||||||
|
self::assertSame('scripts/install', $this->normalizer->repositoryPath('/scripts/install'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRepositoryPathRejectsTraversal(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
|
||||||
|
$this->normalizer->repositoryPath('../install.sh');
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue