편집기 헤더 「로고 이미지」에 파일을 올린 뒤 저장하면 「HTTPS 프로토콜 URL은 허용되지 않습니다」 422 로 거부되던 결함. 프록시 서빙 URL 이 절대 형태로 발급되어 NoExternalUrls 가 서버 자신의 주소를 외부로 차단했고, 배경 이미지는 style 로 들어가 스캔 밖이어서 로고(propValue)에서만 드러났다. - proxyUrl 을 사이트 상대 경로로 발급 (발급 시점 도메인·스킴에 묶이지 않는다) - SiteAssetHosts 신설: app.url host + 운영자가 선언한 공개 자산 디스크 host 를 브라우저와 같은 정규화 뒤 등가 비교. 요청 Host 헤더는 근거로 쓰지 않고, 흉내 host·protocol-relative·위험 스킴은 종전대로 차단 - 회귀 테스트 4클래스(71 passed) + 시나리오 매니페스트 + Playwright 2케이스, 공개 문서(SECURITY·security·storage-driver·api)와 규정 표 갱신 함께 담긴 릴리즈 정비: - 7.0.11 미출시 섹션의 릴리즈 날짜를 09-09 로 갱신 (코어·board·page·admin_basic·basic·ja 언어팩 3종) - 배포본에 실리는 주석·독블록·시나리오의 내부 호칭을 중립 표현으로 정리하고, 동봉 vendor 자산의 우연한 토큰은 누출 검사에서 경로 규칙으로만 제외 - 버전 bump 검사: github_url 없는 번들을 언어팩과 같은 origin/release 페이로드 기준선으로 판정(상시 경고 제거), 코어 버전 판독이 env 래핑을 읽도록 보정
134 lines
5.2 KiB
PHP
134 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Support;
|
|
|
|
use App\Support\SiteAssetHosts;
|
|
use Illuminate\Support\Facades\Config;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* 사이트 자산 host 판정 단위 테스트
|
|
*
|
|
* 저장 게이트(NoExternalUrls)가 "서버가 스스로 발급하는 자산 주소" 를 외부로 오판하지
|
|
* 않도록, 사이트 자기 host(app.url)와 운영자가 선언한 공개 자산 디스크의 host 를 한
|
|
* 지점에서 해석한다. 요청 Host 헤더는 근거로 쓰지 않는다 — 위조 가능하다.
|
|
*/
|
|
class SiteAssetHostsTest extends TestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
Config::set('app.url', 'https://shop.example.test');
|
|
Config::set('core.storage.public_asset_disk', '');
|
|
}
|
|
|
|
/**
|
|
* app.url 의 host 는 항상 포함되어야 합니다 (소문자 정규화).
|
|
*
|
|
* @scenario url_host=own_site_absolute
|
|
*
|
|
* @effects issued_asset_url_passes_storage_gate
|
|
*/
|
|
public function test_hosts_include_app_url_host(): void
|
|
{
|
|
Config::set('app.url', 'https://Shop.Example.TEST:8443/sub');
|
|
|
|
$this->assertContains('shop.example.test', SiteAssetHosts::hosts());
|
|
}
|
|
|
|
/**
|
|
* 공개 자산 디스크가 선언되어 있으면 그 디스크의 기준 URL host 가 포함되어야 합니다.
|
|
*
|
|
* @scenario url_host=public_asset_disk
|
|
*
|
|
* @effects issued_asset_url_passes_storage_gate
|
|
*/
|
|
public function test_hosts_include_declared_public_asset_disk_host(): void
|
|
{
|
|
Config::set('core.storage.public_asset_disk', 'public');
|
|
Config::set('filesystems.disks.public.url', 'https://cdn.example.test/assets');
|
|
|
|
$this->assertContains('cdn.example.test', SiteAssetHosts::hosts());
|
|
}
|
|
|
|
/**
|
|
* 미선언·'none'·고아 디스크는 host 를 보태지 않아야 합니다 — 판정은 PublicAssetDisk::resolve()
|
|
* 를 경유하므로 디스크에 url 이 설정돼 있어도 선언 없이는 외부다.
|
|
*
|
|
* @scenario url_host=external_undeclared
|
|
*
|
|
* @effects external_host_still_rejected
|
|
*/
|
|
public function test_undeclared_disk_host_is_not_included(): void
|
|
{
|
|
Config::set('filesystems.disks.public.url', 'https://cdn.example.test/assets');
|
|
|
|
foreach (['', 'none', 'vanished_plugin_disk'] as $declared) {
|
|
Config::set('core.storage.public_asset_disk', $declared);
|
|
|
|
$this->assertNotContains('cdn.example.test', SiteAssetHosts::hosts(), "선언값 '{$declared}' 에서 host 가 새면 안 됩니다");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* http/https 절대 URL 이고 host 가 목록에 있을 때만 참이어야 합니다.
|
|
*
|
|
* @scenario url_host=own_site_absolute
|
|
*
|
|
* @effects issued_asset_url_passes_storage_gate
|
|
*/
|
|
public function test_is_site_asset_url_accepts_only_http_urls_on_listed_hosts(): void
|
|
{
|
|
$this->assertTrue(SiteAssetHosts::isSiteAssetUrl('https://shop.example.test/api/x'));
|
|
$this->assertTrue(SiteAssetHosts::isSiteAssetUrl('http://shop.example.test/storage/x.png'));
|
|
$this->assertTrue(SiteAssetHosts::isSiteAssetUrl(" https://shop.example.test/x\n"));
|
|
|
|
// 경로만 있는 값은 host 가 없으므로 이 판정의 대상이 아니다 (호출측이 경로 규칙으로 다룬다)
|
|
$this->assertFalse(SiteAssetHosts::isSiteAssetUrl('/api/x'));
|
|
// protocol-relative 는 별도 판정 축 — 자기 host 라도 여기서는 참이 아니다
|
|
$this->assertFalse(SiteAssetHosts::isSiteAssetUrl('//shop.example.test/x'));
|
|
// 위험 스킴은 host 와 무관
|
|
$this->assertFalse(SiteAssetHosts::isSiteAssetUrl('ftp://shop.example.test/x'));
|
|
$this->assertFalse(SiteAssetHosts::isSiteAssetUrl('javascript://shop.example.test/%0aalert(1)'));
|
|
}
|
|
|
|
/**
|
|
* 자기 host 를 흉내 낸 URL 은 거짓이어야 합니다 — 브라우저와 같은 정규화 뒤 host 를 비교한다.
|
|
*
|
|
* @scenario url_host=lookalike
|
|
*
|
|
* @effects external_host_still_rejected
|
|
*/
|
|
public function test_is_site_asset_url_rejects_lookalikes(): void
|
|
{
|
|
foreach ([
|
|
'https://shop.example.test.evil.com/x',
|
|
'https://shop.example.test@evil.com/x',
|
|
'https://evil.com\\@shop.example.test/x',
|
|
'https://evil.com/shop.example.test/x',
|
|
'https://evil.com#shop.example.test',
|
|
"https://evil.com\t.shop.example.test/x",
|
|
] as $url) {
|
|
$this->assertFalse(SiteAssetHosts::isSiteAssetUrl($url), "흉내 URL 은 거짓이어야 합니다: {$url}");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* app.url 이 비어 있거나 host 를 못 읽는 값이면 빈 목록이어야 합니다 (예외 없음).
|
|
*
|
|
* @scenario url_host=external_undeclared
|
|
*
|
|
* @effects external_host_still_rejected
|
|
*/
|
|
public function test_blank_app_url_yields_no_host(): void
|
|
{
|
|
foreach (['', 'not a url', '/relative'] as $value) {
|
|
Config::set('app.url', $value);
|
|
|
|
$this->assertSame([], SiteAssetHosts::hosts(), "app.url='{$value}' 에서 host 가 생기면 안 됩니다");
|
|
$this->assertFalse(SiteAssetHosts::isSiteAssetUrl('https://shop.example.test/x'));
|
|
}
|
|
}
|
|
}
|