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종 신설.
This commit is contained in:
HeuJung committed 2026-08-17 01:45:09 +09:00
1 parent cd84b63d2b
commit 71abdeaf60
249 files changed
+14354 -1134

No files matched your search

+119 -14
View File
@@ -5,6 +5,7 @@ namespace Tests\Unit\Services;
use App\Enums\ExtensionOwnerType;
use App\Enums\PermissionType;
use App\Exceptions\CannotDeleteSuperAdminException;
use App\Exceptions\PermissionEscalationException;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
@@ -506,15 +507,20 @@ class UserServiceTest extends TestCase
}
/**
* core.permissions.update 권한 없으면 타인의 역할 변경 불가 (무시됨)
* 역할 부여는 core.permissions.update(역할 정의 수정)를 요구하지 않는다 — 상한(ceiling)
* 이내라면 그 권한이 없어도 역할을 부여할 수 있다. (역할 부여 = 사용자 관리의 일부)
*
* 회귀 방지: 과거에는 core.permissions.update 가 없으면 role_ids 를 조용히 버려(silent
* no-op) 200 성공을 반환하면서도 역할이 바뀌지 않았다. 이제는 상한만 통과하면 부여된다.
*/
public function test_update_user_ignores_role_ids_without_permission(): void
public function test_update_user_assigns_role_within_ceiling_without_permissions_update(): void
{
$originalRole = Role::create([
'identifier' => 'original-role',
'name' => ['ko' => '원래 역할', 'en' => 'Original Role'],
]);
// 권한이 전혀 없는 역할 → 어떤 액터의 상한도 넘지 않는다.
$newRole = Role::create([
'identifier' => 'new-role-test',
'name' => ['ko' => '새 역할', 'en' => 'New Role'],
@@ -523,20 +529,56 @@ class UserServiceTest extends TestCase
$targetUser = User::factory()->create();
$targetUser->roles()->attach($originalRole->id);
// 권한 없는 사용자로 인증
// core.permissions.update 가 없는 액터로 인증 (사용자 관리 권한은 라우트 단계에서
// 강제되며, 서비스 계층 테스트에서는 상한만 검사된다)
$authUser = User::factory()->create();
Auth::login($authUser);
// 타인의 역할 변경 시도
$updatedUser = $this->userService->updateUser($targetUser, [
'name' => 'Changed Name',
'role_ids' => [$newRole->id],
]);
// 이름은 변경되지만 역할은 변경되지 않아야 함
// 이름과 역할 모두 변경되어야 한다 (silent no-op 아님)
$this->assertEquals('Changed Name', $updatedUser->name);
$this->assertTrue($updatedUser->roles->contains('id', $originalRole->id));
$this->assertFalse($updatedUser->roles->contains('id', $newRole->id));
$this->assertTrue($updatedUser->roles->contains('id', $newRole->id));
$this->assertFalse($updatedUser->roles->contains('id', $originalRole->id));
}
/**
* 상한을 넘는 역할(액터가 보유하지 않은 권한을 담은 역할)은 부여 시 명시적으로 거부된다.
*
* 회귀 방지: 조용히 무시(silent no-op)하는 대신 PermissionEscalationException 을 던져
* 컨트롤러가 403 으로 매핑하도록 한다.
*/
public function test_update_user_rejects_role_beyond_actor_ceiling(): void
{
$elevatedPermission = Permission::create([
'identifier' => 'core.templates.uninstall',
'name' => ['ko' => '템플릿 삭제', 'en' => 'Uninstall Templates'],
'extension_type' => ExtensionOwnerType::Core,
'extension_identifier' => 'core',
'type' => PermissionType::Admin,
]);
// 액터가 보유하지 않은 권한을 담은 역할 → 상한 초과
$elevatedRole = Role::create([
'identifier' => 'elevated-role',
'name' => ['ko' => '상위 역할', 'en' => 'Elevated Role'],
]);
$elevatedRole->permissions()->attach($elevatedPermission->id);
$targetUser = User::factory()->create();
// 그 권한을 보유하지 않은 액터
$authUser = User::factory()->create();
Auth::login($authUser);
$this->expectException(PermissionEscalationException::class);
$this->userService->updateUser($targetUser, [
'role_ids' => [$elevatedRole->id],
]);
}
/**
@@ -591,11 +633,13 @@ class UserServiceTest extends TestCase
// ========================================================================
/**
* core.permissions.update 권한 없으면 기본 역할(user) 자동 할당
* 역할을 지정하지 않고 생성하면 기본 역할('user')이 자동 배정된다.
*
* 회귀 방지: 기본 역할 배정은 권한 게이트가 아니라 "요청 역할 없음" 조건에 묶인다.
* (과거에는 core.permissions.update 미보유 조건에 묶여 있어 지정 역할을 덮어썼다)
*/
public function test_create_user_assigns_default_role_without_permission(): void
public function test_create_user_assigns_default_role_when_none_requested(): void
{
// 기본 user 역할 생성
$userRole = Role::create([
'identifier' => 'user',
'name' => ['ko' => '사용자', 'en' => 'User'],
@@ -604,12 +648,41 @@ class UserServiceTest extends TestCase
'is_active' => true,
]);
$authUser = User::factory()->create();
Auth::login($authUser);
$user = $this->userService->createUser([
'name' => 'Test User',
'email' => 'testcreate@test.com',
'password' => 'password123',
]);
$this->assertTrue($user->roles->contains('id', $userRole->id));
}
/**
* core.permissions.update 가 없어도 상한 이내의 지정 역할은 그대로 부여된다
* (기본 역할로 덮어쓰지 않는다).
*
* 회귀 방지: 과거에는 core.permissions.update 미보유 시 지정 역할을 버리고 기본 역할을
* 강제 배정했다. 이제는 상한만 통과하면 지정 역할이 부여된다.
*/
public function test_create_user_assigns_requested_role_within_ceiling_without_permissions_update(): void
{
$userRole = Role::create([
'identifier' => 'user',
'name' => ['ko' => '사용자', 'en' => 'User'],
'extension_type' => ExtensionOwnerType::Core,
'extension_identifier' => 'core',
'is_active' => true,
]);
// 권한이 없는 역할 → 상한 이내
$customRole = Role::create([
'identifier' => 'custom-role',
'name' => ['ko' => '커스텀 역할', 'en' => 'Custom Role'],
]);
// 권한 없는 사용자로 인증
$authUser = User::factory()->create();
Auth::login($authUser);
@@ -620,9 +693,41 @@ class UserServiceTest extends TestCase
'role_ids' => [$customRole->id],
]);
// 커스텀 역할 대신 기본 역할이 할당되어야 함
$this->assertTrue($user->roles->contains('id', $userRole->id));
$this->assertFalse($user->roles->contains('id', $customRole->id));
// 지정 역할이 부여되고 기본 역할로 덮어쓰이지 않는다
$this->assertTrue($user->roles->contains('id', $customRole->id));
$this->assertFalse($user->roles->contains('id', $userRole->id));
}
/**
* 상한을 넘는 역할을 지정해 생성하면 명시적으로 거부된다.
*/
public function test_create_user_rejects_role_beyond_actor_ceiling(): void
{
$elevatedPermission = Permission::create([
'identifier' => 'core.templates.uninstall',
'name' => ['ko' => '템플릿 삭제', 'en' => 'Uninstall Templates'],
'extension_type' => ExtensionOwnerType::Core,
'extension_identifier' => 'core',
'type' => PermissionType::Admin,
]);
$elevatedRole = Role::create([
'identifier' => 'elevated-create-role',
'name' => ['ko' => '상위 역할', 'en' => 'Elevated Role'],
]);
$elevatedRole->permissions()->attach($elevatedPermission->id);
$authUser = User::factory()->create();
Auth::login($authUser);
$this->expectException(PermissionEscalationException::class);
$this->userService->createUser([
'name' => 'New User',
'email' => 'newuser@test.com',
'password' => 'password123',
'role_ids' => [$elevatedRole->id],
]);
}
/**