Files
Gnuboard7/tests/Unit/Repositories/IdentityVerificationLogRepositoryTest.php
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

164 lines
5.0 KiB
PHP

<?php
namespace Tests\Unit\Repositories;
use App\Contracts\Repositories\IdentityVerificationLogRepositoryInterface;
use App\Enums\IdentityVerificationStatus;
use App\Models\IdentityVerificationLog;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Tests\TestCase;
/**
* IdentityVerificationLogRepository 의 검증 토큰 조회 만료 검증.
*
* 회귀 배경(KVE-2026-2029): findVerifiedForToken() 이 status/consumed_at 만 보고
* expires_at 술어를 갖지 않아, 만료된 본인인증 토큰이 무기한 유효하게 취급됐다.
* 이 저장소를 공유하는 소비자(정책 미들웨어·정책 서비스·회원가입/비밀번호재설정
* 리스너·IdvTokenRule)가 전부 같은 결함을 물려받았으므로 저장소 단일 지점에서 닫는다.
*
* @group identity
* @group unit
*/
class IdentityVerificationLogRepositoryTest extends TestCase
{
use DatabaseTransactions;
private IdentityVerificationLogRepositoryInterface $repository;
protected function setUp(): void
{
parent::setUp();
$this->repository = app(IdentityVerificationLogRepositoryInterface::class);
}
/**
* 검증 완료 로그를 생성합니다.
*
* @param array<string, mixed> $overrides 덮어쓸 속성
*/
private function makeVerifiedLog(array $overrides = []): IdentityVerificationLog
{
return IdentityVerificationLog::create(array_merge([
'provider_id' => 'test-provider',
'purpose' => 'sensitive_action',
'channel' => 'email',
'user_id' => null,
'target_hash' => hash('sha256', 'kve2029-'.Str::random(8)),
'status' => IdentityVerificationStatus::Verified->value,
'attempts' => 1,
'max_attempts' => 5,
'verification_token' => 'kve2029-'.Str::random(32),
'expires_at' => Carbon::now()->addMinutes(10),
'verified_at' => Carbon::now()->subMinute(),
'consumed_at' => null,
], $overrides));
}
/**
* 미만료 검증 토큰은 조회된다 (정상 흐름 불변).
*/
public function test_unexpired_verified_token_is_returned(): void
{
$log = $this->makeVerifiedLog();
$found = $this->repository->findVerifiedForToken(
$log->verification_token,
'sensitive_action'
);
$this->assertNotNull($found);
$this->assertSame($log->id, $found->id);
}
/**
* 만료된 검증 토큰은 조회되지 않는다 (KVE-2026-2029).
*/
public function test_expired_verified_token_is_not_returned(): void
{
$log = $this->makeVerifiedLog([
'expires_at' => Carbon::now()->subMinute(),
]);
$found = $this->repository->findVerifiedForToken(
$log->verification_token,
'sensitive_action'
);
$this->assertNull($found);
}
/**
* 만료 경계: 1초 전은 통과, 1초 후는 차단.
*/
public function test_expiry_boundary_is_closed_strictly(): void
{
$valid = $this->makeVerifiedLog([
'expires_at' => Carbon::now()->addSecond(),
]);
$expired = $this->makeVerifiedLog([
'expires_at' => Carbon::now()->subSecond(),
]);
$this->assertNotNull(
$this->repository->findVerifiedForToken($valid->verification_token, 'sensitive_action')
);
$this->assertNull(
$this->repository->findVerifiedForToken($expired->verification_token, 'sensitive_action')
);
}
/**
* expires_at 이 비어 있는 토큰은 조회되지 않는다 (엄격 차단).
*
* 보안 토큰은 모든 provider 가 challenge 생성 시 expires_at 을 세팅한다.
* NULL 은 정상 발급 산물이 아니므로 허용 분기를 두지 않는다.
*/
public function test_token_without_expiry_is_not_returned(): void
{
$log = $this->makeVerifiedLog([
'expires_at' => null,
]);
$found = $this->repository->findVerifiedForToken(
$log->verification_token,
'sensitive_action'
);
$this->assertNull($found);
}
/**
* 이미 소비된 토큰은 조회되지 않는다 (기존 동작 보존).
*/
public function test_consumed_token_is_not_returned(): void
{
$log = $this->makeVerifiedLog([
'consumed_at' => Carbon::now(),
]);
$found = $this->repository->findVerifiedForToken(
$log->verification_token,
'sensitive_action'
);
$this->assertNull($found);
}
/**
* 목적(purpose)이 다르면 조회되지 않는다 (기존 동작 보존).
*/
public function test_token_with_other_purpose_is_not_returned(): void
{
$log = $this->makeVerifiedLog();
$found = $this->repository->findVerifiedForToken(
$log->verification_token,
'signup'
);
$this->assertNull($found);
}
}