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