Files
Gnuboard7/tests/Unit/Installer/EnvValueSerializationTest.php
T
HeuJung 9cf9c3ff8c fix(core,board,ecommerce,payments,basic): KVE-2026 보안 게이트 6묶음 + 자격증명 전송로 정합
KISA 제보 취약점(KVE-2026-1914/1919/2019/2029/2041/2042/2043/2044)과
그 수정 과정에서 드러난 자격증명 전송로 결함을 함께 해소한다.

globalHeaders 는 데이터소스와 apiCall 핸들러에만 적용되는데, 코어 ApiClient 를
직접 부르는 경로들이 그 사실을 모른 채 게이트된 엔드포인트를 호출하고 있었다.
서버는 정당한 사용자를 거부하고 화면은 이미 버튼을 내준 뒤라, 예외도 로그도 없이
그 자리만 비는 형태로만 드러났다. 전송로 10축을 전수 열거해 6건을 고치고,
같은 실수가 반복되지 않도록 규정과 coverage 에 등재했다.

아웃바운드 프록시가 사이트 자기 자신으로 가는 내부 요청까지 가로채 저장이 수십 초씩
걸리던 문제도 함께 고쳤다. 실패가 폴백으로 삼켜져 화면에는 지연으로만 나타났다.
2026-09-06 00:42:29 +09:00

154 lines
5.9 KiB
PHP

<?php
namespace Tests\Unit\Installer;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
/**
* 인스톨러 .env 값 직렬화 테스트 (KVE-2026-2042).
*
* 회귀 배경: `.env` 는 줄 단위 형식이라 사용자 입력에 개행이 섞이면 그 뒤가 새로운
* 환경변수 줄로 해석된다. 기존 escapeEnvValue() 는 개행을 조용히 지웠고 그마저도
* 일부 값(호스트·DB명·앱 이름·업데이트 URL 등)에는 적용되지 않아, 설치 창이 열려 있는
* 동안 임의 환경변수를 주입할 수 있었다.
*
* 정책: 직렬화기는 CR/LF/NUL 을 만나면 예외를 던진다(조용한 삭제 금지 — 삭제하면
* 운영자가 자기가 입력한 값과 다른 값이 저장된 것을 알 수 없다). 값 전달 경로가
* 하나이므로 새 필드가 추가돼도 같은 관문을 지난다.
*
* 정책 파일은 의존성이 없어야 한다 — 인스톨러 본 흐름과 워커 양쪽에서 단독으로
* require 되므로, 이 테스트도 BASE_PATH·lang 스텁 없이 파일만 로드한다.
*/
class EnvValueSerializationTest extends TestCase
{
public static function setUpBeforeClass(): void
{
require_once dirname(__DIR__, 3).'/public/install/includes/env-value.php';
}
/**
* 정상 값은 큰따옴표로 감싸 반환된다.
*/
#[Test]
public function it_quotes_a_normal_value(): void
{
$this->assertSame('"g7"', serializeEnvValue('g7'));
$this->assertSame('"127.0.0.1"', serializeEnvValue('127.0.0.1'));
}
/**
* 빈 값은 빈 따옴표로 반환된다 (기존 동작 보존).
*/
#[Test]
public function it_returns_empty_quotes_for_an_empty_value(): void
{
$this->assertSame('""', serializeEnvValue(''));
}
/**
* 큰따옴표와 백슬래시는 이스케이프된다 (기존 동작 보존).
*/
#[Test]
public function it_escapes_quotes_and_backslashes(): void
{
$this->assertSame('"a\\\\b"', serializeEnvValue('a\\b'));
$this->assertSame('"say \\"hi\\""', serializeEnvValue('say "hi"'));
}
/**
* 줄바꿈·NUL 이 섞인 값은 예외로 거부된다 (조용한 삭제 금지).
*
* @param string $value 주입 시도 값
* @param string $reason 거부 사유 (실패 메시지용)
*/
#[Test]
#[DataProvider('lineBreakInjectionProvider')]
public function it_rejects_values_containing_line_breaks_or_nul(string $value, string $reason): void
{
$this->expectException(InvalidArgumentException::class);
serializeEnvValue($value);
}
/**
* 개행 주입 벡터.
*
* @return array<string, array{0: string, 1: string}>
*/
public static function lineBreakInjectionProvider(): array
{
return [
'LF 로 새 변수 줄 주입' => ["g7\nAPP_DEBUG=true", 'LF'],
'CR 로 새 변수 줄 주입' => ["g7\rAPP_DEBUG=true", 'CR'],
'CRLF 로 새 변수 줄 주입' => ["g7\r\nAPP_DEBUG=true", 'CRLF'],
'NUL 바이트' => ["g7\0APP_DEBUG=true", 'NUL'],
'선두 개행' => ["\nAPP_DEBUG=true", 'leading LF'],
];
}
/**
* 개행 검사는 값 전체를 본다 — 따옴표 안에 숨겨도 거부된다.
*/
#[Test]
public function it_rejects_line_breaks_even_inside_quotes(): void
{
$this->expectException(InvalidArgumentException::class);
serializeEnvValue("\"g7\"\nAPP_DEBUG=true");
}
/**
* 하위호환 별칭 `escapeEnvValue` 의 두 정의가 모두 단일 관문에 위임한다.
*
* 종전 구현은 개행을 **조용히 지웠다**. 그 동작이 어느 한쪽에라도 남아 있으면 다음
* 호출자가 그 이름을 불러 같은 결함을 다시 들여온다 — 실제로 과거 KVE 수정들이
* "한쪽만 고쳐서" 재수정된 이력이 있다.
*
* 함수를 실행해 대조하지 않고 소스를 검사하는 이유: 두 정의는 BASE_PATH 상수를
* 요구하는 파일에 있고, 그 상수를 여기서 박으면 인접 Installer 테스트의 안전 가드가
* 걸려 그 테스트들이 조용히 skip 된다(초록인데 검사는 멈춘 상태가 된다).
*/
#[Test]
public function both_legacy_alias_definitions_delegate_to_the_single_gate(): void
{
$projectRoot = dirname(__DIR__, 3);
$definitions = [
'includes/functions.php' => $projectRoot.'/public/install/includes/functions.php',
'includes/installer-runtime.php' => $projectRoot.'/public/install/includes/installer-runtime.php',
];
foreach ($definitions as $label => $path) {
$source = (string) file_get_contents($path);
$this->assertMatchesRegularExpression(
'/function escapeEnvValue\(string \$value\): string\s*\{[^}]*return serializeEnvValue\(\$value\);/s',
$source,
"{$label} 의 escapeEnvValue 가 단일 관문에 위임하지 않는다"
);
$this->assertStringNotContainsString(
'str_replace(["\r", "\n"], \'\', $value)',
$source,
"{$label} 에 개행을 조용히 지우는 경로가 남아 있다 — 그 이름을 부르면 결함이 재유입된다"
);
}
}
/**
* 개행이 없는 값은 검사기가 통과시킨다.
*/
#[Test]
public function the_line_break_checker_accepts_clean_values(): void
{
$this->assertTrue(installer_env_value_is_single_line('https://github.com/gnuboard/g7'));
$this->assertTrue(installer_env_value_is_single_line(''));
$this->assertFalse(installer_env_value_is_single_line("a\nb"));
$this->assertFalse(installer_env_value_is_single_line("a\rb"));
$this->assertFalse(installer_env_value_is_single_line("a\0b"));
}
}