Files
Gnuboard7/modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageAttachmentAccessTest.php
T
HeuJung 71abdeaf60 fix(security): KVE-2026-1914/1915/1919 remediation 전건
위임 관리자(부관리자)가 권한·역할·표현식·비밀 콘텐츠 경계를 우회하던
결함군을 계층 대칭성 원칙으로 전건 차단한다. 약한 경로가 정상 응답을
내보내는 것이 유일한 증상이라, 게이트를 생산 지점 한 곳(SSoT)에 두고
같은 데이터를 내보내는 소비 경로 전부가 그 게이트를 경유하도록 맞췄다.

- 등급 상한(rank ceiling): 슈퍼관리자 보호·역할/사용자 역할 배정(추가·제거
 대칭)·일괄 상태변경·순서변경을 상세 경로와 동일 강도로 재적용. 가드는
 DB 쓰기에 선행하여 거부 시 상태 불변.
- 레이아웃 표현식: new Function/with 실행을 AST 화이트리스트 평가기로 교체.
 비-문자열 computed 키 정규화(normalizeKey)·Object facade(리플렉션 static
 제거)·legacy 접근자 차단. 저장측 검증·정적 검사와 3계층 동형.
- secret 게이트: 비밀글의 댓글·첨부·문의 독립 경로 재적용, hash 파일서빙
 소유권·비밀·발행 상태 검사 통일.
- 신뢰 스크립트 호스트: 확장 선언 기반 + same-origin 브라우저 정규화를
 런타임·저장측·정적검사 3층 동형화.
- 회귀 감지: 단위·Feature·E2E·시나리오 매니페스트 전축 + audit 룰 4종 신설.
2026-08-17 01:45:09 +09:00

199 lines
7.7 KiB
PHP

<?php
namespace Modules\Sirsoft\Page\Tests\Feature\User;
// FeatureTestCase를 수동으로 require (autoload 전에 로드 필요)
require_once __DIR__.'/../../FeatureTestCase.php';
use App\Models\User;
use Mockery;
use Modules\Sirsoft\Page\Models\Page;
use Modules\Sirsoft\Page\Models\PageAttachment;
use Modules\Sirsoft\Page\Services\PageAttachmentService;
use Modules\Sirsoft\Page\Tests\FeatureTestCase;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* 공개 페이지 첨부 미리보기/다운로드 접근 게이트 테스트 (KVE-2026-1914 A-6 S-1)
*
* preview() 는 과거 발행상태 무관 공개 서빙이라, 미발행(초안) 페이지의 첨부 썸네일을
* hash 만 있으면 무인가로 볼 수 있었다(preview↔download 게이트 불일치). 이 클래스는
* preview 가 download 와 동일하게 "발행 첨부는 공개, 미발행 첨부는 pages.read 관리자만"
* 게이트를 적용하는지 실제 HTTP 요청으로 고정한다.
*
* 게이트 통과/차단을 응답코드로 명확히 구분하기 위해 Service 를 모킹한다 —
* 게이트 통과 시 200(스트리밍), 게이트 차단 시 컨트롤러가 Service 호출 전에 404 를 낸다.
*/
class PublicPageAttachmentAccessTest extends FeatureTestCase
{
private const BASE = '/api/modules/sirsoft-page/pages/attachment';
protected function tearDown(): void
{
Page::where('slug', 'like', 'attach-access-%')->forceDelete();
Mockery::close();
parent::tearDown();
}
/**
* Service 를 모킹해 게이트 통과 시 200 을 반환하도록 만든다.
*
* @param PageAttachment $attachment getByHash 가 반환할 첨부
*/
private function mockServicePassThrough(PageAttachment $attachment): void
{
$svc = Mockery::mock(PageAttachmentService::class);
$svc->shouldReceive('getByHash')->andReturn($attachment);
$svc->shouldReceive('preview')->andReturn(new StreamedResponse(fn () => null, 200));
$svc->shouldReceive('download')->andReturn(new StreamedResponse(fn () => null, 200));
$this->app->instance(PageAttachmentService::class, $svc);
}
/**
* 첨부를 만든다.
*
* @param bool $published 부모 페이지 발행 여부
* @param string $slug 페이지 슬러그
* @return PageAttachment 생성된 첨부
*/
private function makeAttachment(bool $published, string $slug): PageAttachment
{
$pageFactory = Page::factory()->state(['slug' => $slug]);
if ($published) {
$pageFactory = $pageFactory->published();
}
$page = $pageFactory->create();
return PageAttachment::factory()->image()->create(['page_id' => $page->id]);
}
// ── preview 게이트 (S-1 핵심) ──────────────────────────
/**
* 미발행 페이지 첨부 미리보기는 비인가 사용자에게 404 로 차단된다 (S-1 수정 핵심)
*
* @scenario resource=page_attachment, parent_state=restricted
*
* @effects unpublished_page_attachment_preview_blocked_for_guest
*/
public function test_unpublished_preview_blocked_for_anonymous(): void
{
$attachment = $this->makeAttachment(false, 'attach-access-draft');
$this->mockServicePassThrough($attachment);
$this->getJson(self::BASE.'/'.$attachment->hash.'/preview')
->assertStatus(404);
}
/**
* 미발행 페이지 첨부 미리보기는 권한 없는 로그인 사용자에게도 404 로 차단된다
*
* @scenario resource=page_attachment, parent_state=restricted
*
* @effects unpublished_page_attachment_preview_blocked_without_permission
*/
public function test_unpublished_preview_blocked_for_user_without_permission(): void
{
$attachment = $this->makeAttachment(false, 'attach-access-draft-user');
$this->mockServicePassThrough($attachment);
$user = User::factory()->create();
$this->actingAs($user)
->getJson(self::BASE.'/'.$attachment->hash.'/preview')
->assertStatus(404);
}
/**
* 미발행 페이지 첨부 미리보기는 pages.read 관리자에게 허용된다 (초안 편집 썸네일 회귀 방지)
*
* @scenario resource=page_attachment, parent_state=restricted
*
* @effects unpublished_page_attachment_preview_allowed_with_pages_read
*/
public function test_unpublished_preview_allowed_for_admin_with_pages_read(): void
{
$attachment = $this->makeAttachment(false, 'attach-access-draft-admin');
$this->mockServicePassThrough($attachment);
$admin = $this->createAdminUser(['sirsoft-page.pages.read']);
$this->actingAs($admin)
->getJson(self::BASE.'/'.$attachment->hash.'/preview')
->assertStatus(200);
}
/**
* 발행된 페이지 첨부 미리보기는 비인가 사용자에게도 공개된다 (정상 흐름 회귀 방지)
*
* @scenario resource=page_attachment, parent_state=public
*
* @effects published_page_attachment_preview_still_public
*/
public function test_published_preview_allowed_for_anonymous(): void
{
$attachment = $this->makeAttachment(true, 'attach-access-live');
$this->mockServicePassThrough($attachment);
$this->getJson(self::BASE.'/'.$attachment->hash.'/preview')
->assertStatus(200);
}
// ── download 게이트 (기존 동작 회귀 대조) ──────────────
/**
* 미발행 페이지 첨부 다운로드는 비인가 사용자에게 404 로 차단된다 (기존 게이트 회귀 대조)
*
* @scenario resource=page_attachment, parent_state=restricted
*
* @effects unpublished_page_attachment_download_blocked_for_guest
*/
public function test_unpublished_download_blocked_for_anonymous(): void
{
$attachment = $this->makeAttachment(false, 'attach-access-dl-draft');
$this->mockServicePassThrough($attachment);
$this->getJson(self::BASE.'/'.$attachment->hash)
->assertStatus(404);
}
/**
* 미발행 페이지 첨부 다운로드는 pages.read 관리자에게 허용된다.
*
* preview 축은 4건(게스트/무권한 회원/관리자/발행글)인데 download 축은 차단 1건뿐이라
* 두 경로의 커버리지 강도가 갈려 있었다. 게이트를 조일 때 download 만 과차단으로
* 회귀해도 스위트가 green 이 되므로 허용 축을 함께 고정한다.
*
* @scenario resource=page_attachment, parent_state=restricted
*
* @effects unpublished_page_attachment_download_allowed_with_pages_read
*/
public function test_unpublished_download_allowed_for_admin_with_pages_read(): void
{
$attachment = $this->makeAttachment(false, 'attach-access-dl-draft-admin');
$this->mockServicePassThrough($attachment);
$admin = $this->createAdminUser(['sirsoft-page.pages.read']);
$this->actingAs($admin)
->getJson(self::BASE.'/'.$attachment->hash)
->assertStatus(200);
}
/**
* 발행된 페이지 첨부 다운로드는 비인가 사용자에게도 공개된다 (정상 흐름 회귀 방지).
*
* @scenario resource=page_attachment, parent_state=public
*
* @effects published_page_attachment_download_still_public
*/
public function test_published_download_allowed_for_anonymous(): void
{
$attachment = $this->makeAttachment(true, 'attach-access-dl-live');
$this->mockServicePassThrough($attachment);
$this->getJson(self::BASE.'/'.$attachment->hash)
->assertStatus(200);
}
}