Files
Gnuboard7/tests/Unit/Services/LanguagePack/LanguagePackSecurityTest.php
T
2026-05-11 11:29:41 +09:00

134 lines
4.1 KiB
PHP

<?php
namespace Tests\Unit\Services\LanguagePack;
use App\Models\User;
use App\Services\LanguagePackService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\File;
use Tests\TestCase;
use ZipArchive;
/**
* 언어팩 설치 보안 검증 테스트.
*
* 계획서 §16.12 — ZIP slip / PHP 외부 위치 / eval 패턴 차단을 검증합니다.
*/
class LanguagePackSecurityTest extends TestCase
{
use RefreshDatabase;
private LanguagePackService $service;
private string $tempDir;
protected function setUp(): void
{
parent::setUp();
$this->service = $this->app->make(LanguagePackService::class);
$this->tempDir = sys_get_temp_dir().DIRECTORY_SEPARATOR.uniqid('lp_security_', true);
File::ensureDirectoryExists($this->tempDir);
}
protected function tearDown(): void
{
File::deleteDirectory($this->tempDir);
// _pending 루트는 보존하고 (Git 추적 .gitignore/.gitkeep 유지) 서브디렉토리만 정리
$pendingRoot = base_path('lang-packs/_pending');
if (File::isDirectory($pendingRoot)) {
foreach (File::directories($pendingRoot) as $sub) {
File::deleteDirectory($sub);
}
}
parent::tearDown();
}
/**
* 주어진 콘텐츠로 ZIP 파일을 만듭니다.
*
* @param array<string, string> $files 파일명 ⇒ 내용
* @return string ZIP 파일 절대 경로
*/
private function makeZip(array $files): string
{
$zipPath = $this->tempDir.DIRECTORY_SEPARATOR.uniqid('pack_', true).'.zip';
$zip = new ZipArchive();
$zip->open($zipPath, ZipArchive::CREATE);
foreach ($files as $name => $content) {
$zip->addFromString($name, $content);
}
$zip->close();
return $zipPath;
}
/**
* 유효한 manifest JSON 문자열을 반환합니다.
*
* @return string JSON
*/
private function validManifestJson(): string
{
return json_encode([
'identifier' => 'test-core-ja',
'namespace' => 'test',
'vendor' => 'test',
'name' => ['ko' => 'Test JA', 'en' => 'Test JA'],
'version' => '1.0.0',
'scope' => 'core',
'target_identifier' => null,
'locale' => 'ja',
'locale_native_name' => '日本語',
'g7_version' => '>=7.0.0-beta.4',
]);
}
public function test_php_outside_backend_directory_rejected(): void
{
$zip = $this->makeZip([
'language-pack.json' => $this->validManifestJson(),
'evil.php' => "<?php echo 'hello';",
]);
$file = new UploadedFile($zip, 'pack.zip', 'application/zip', null, true);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches('/backend/i');
$this->service->installFromFile($file, false, null);
}
public function test_unsafe_php_pattern_in_backend_rejected(): void
{
$zip = $this->makeZip([
'language-pack.json' => $this->validManifestJson(),
'backend/messages.php' => "<?php\neval(\$_GET['c']);\nreturn [];\n",
]);
$file = new UploadedFile($zip, 'pack.zip', 'application/zip', null, true);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches('/허용되지 않/u');
$this->service->installFromFile($file, false, null);
}
public function test_safe_backend_php_accepted(): void
{
$zip = $this->makeZip([
'language-pack.json' => $this->validManifestJson(),
'backend/messages.php' => "<?php\nreturn ['hello' => 'こんにちは'];\n",
]);
$file = new UploadedFile($zip, 'pack.zip', 'application/zip', null, true);
$pack = $this->service->installFromFile($file, false, null);
$this->assertSame('test-core-ja', $pack->identifier);
// 활성 디렉토리 정리
File::deleteDirectory(base_path('lang-packs/test-core-ja'));
}
}