135 lines
4.6 KiB
PHP
135 lines
4.6 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Revision Storage Security Unit Tests
|
|
*
|
|
* @author Ball Studios <https://git.balls.studio>
|
|
* @license MIT
|
|
*/
|
|
|
|
namespace Tests\Addons\Unit;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use Pterodactyl\Addons\AdvancedAdmin\Services\Revisions\RevisionStorageService;
|
|
use Pterodactyl\Addons\AdvancedAdmin\Services\Revisions\ExclusionMatcher;
|
|
|
|
class RevisionStorageTest extends TestCase
|
|
{
|
|
private RevisionStorageService $storage;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->storage = new RevisionStorageService();
|
|
}
|
|
|
|
// ── Path Traversal Prevention ─────────────────────────────────────────────
|
|
|
|
public function test_rejects_absolute_path(): void
|
|
{
|
|
$this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class);
|
|
$this->storage->guardFilePath('/etc/passwd');
|
|
}
|
|
|
|
public function test_rejects_dotdot_traversal(): void
|
|
{
|
|
$this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class);
|
|
$this->storage->guardFilePath('../../../etc/passwd');
|
|
}
|
|
|
|
public function test_rejects_encoded_traversal(): void
|
|
{
|
|
$this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class);
|
|
$this->storage->guardFilePath('./../../secret');
|
|
}
|
|
|
|
public function test_accepts_valid_relative_path(): void
|
|
{
|
|
$result = $this->storage->guardFilePath('config/server.properties');
|
|
$this->assertEquals('config/server.properties', $result);
|
|
}
|
|
|
|
// ── Binary Detection ──────────────────────────────────────────────────────
|
|
|
|
public function test_detects_binary_content(): void
|
|
{
|
|
$binary = "MZ\x90\x00\x03\x00\x00\x00"; // PE header
|
|
$this->assertTrue($this->storage->isBinary($binary));
|
|
}
|
|
|
|
public function test_accepts_text_content(): void
|
|
{
|
|
$text = "# Config file\nserver-name=My Server\nmax-players=20\n";
|
|
$this->assertFalse($this->storage->isBinary($text));
|
|
}
|
|
|
|
// ── Content Hashing ───────────────────────────────────────────────────────
|
|
|
|
public function test_content_hash_is_consistent(): void
|
|
{
|
|
$content = "hello world";
|
|
$hash1 = $this->storage->contentHash($content);
|
|
$hash2 = $this->storage->contentHash($content);
|
|
$this->assertEquals($hash1, $hash2);
|
|
}
|
|
|
|
public function test_different_content_produces_different_hash(): void
|
|
{
|
|
$h1 = $this->storage->contentHash("content A");
|
|
$h2 = $this->storage->contentHash("content B");
|
|
$this->assertNotEquals($h1, $h2);
|
|
}
|
|
|
|
// ── Path Hash is Server-Scoped ─────────────────────────────────────────────
|
|
|
|
public function test_path_hash_is_server_scoped(): void
|
|
{
|
|
$hash1 = $this->storage->pathHash(1, 'config.yml');
|
|
$hash2 = $this->storage->pathHash(2, 'config.yml');
|
|
$this->assertNotEquals($hash1, $hash2, 'Same file on different servers must have different path hashes');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exclusion Matcher Tests
|
|
*/
|
|
class ExclusionMatcherTest extends TestCase
|
|
{
|
|
private function matcher(array $patterns = []): ExclusionMatcher
|
|
{
|
|
// Override config for testing
|
|
config(['advanced-admin.revisions.excluded_patterns' => $patterns ?: [
|
|
'.env', '*.key', '*.pem', '*.p12', 'secrets.yml', '*.sql', '*.bak',
|
|
]]);
|
|
return new ExclusionMatcher();
|
|
}
|
|
|
|
public function test_excludes_dotenv(): void
|
|
{
|
|
$this->assertTrue($this->matcher()->isExcluded('.env'));
|
|
}
|
|
|
|
public function test_excludes_key_files(): void
|
|
{
|
|
$this->assertTrue($this->matcher()->isExcluded('private.key'));
|
|
$this->assertTrue($this->matcher()->isExcluded('server.pem'));
|
|
}
|
|
|
|
public function test_excludes_sql_dumps(): void
|
|
{
|
|
$this->assertTrue($this->matcher()->isExcluded('backup.sql'));
|
|
}
|
|
|
|
public function test_allows_normal_config(): void
|
|
{
|
|
$this->assertFalse($this->matcher()->isExcluded('server.properties'));
|
|
$this->assertFalse($this->matcher()->isExcluded('config/spigot.yml'));
|
|
}
|
|
|
|
public function test_excludes_sensitive_keyword_in_name(): void
|
|
{
|
|
$this->assertTrue($this->matcher()->isExcluded('my-secret-key.txt'));
|
|
$this->assertTrue($this->matcher()->isExcluded('password-store.txt'));
|
|
}
|
|
}
|