KISA 제보 취약점(KVE-2026-1914/1919/2019/2029/2041/2042/2043/2044)과 그 수정 과정에서 드러난 자격증명 전송로 결함을 함께 해소한다. globalHeaders 는 데이터소스와 apiCall 핸들러에만 적용되는데, 코어 ApiClient 를 직접 부르는 경로들이 그 사실을 모른 채 게이트된 엔드포인트를 호출하고 있었다. 서버는 정당한 사용자를 거부하고 화면은 이미 버튼을 내준 뒤라, 예외도 로그도 없이 그 자리만 비는 형태로만 드러났다. 전송로 10축을 전수 열거해 6건을 고치고, 같은 실수가 반복되지 않도록 규정과 coverage 에 등재했다. 아웃바운드 프록시가 사이트 자기 자신으로 가는 내부 요청까지 가로채 저장이 수십 초씩 걸리던 문제도 함께 고쳤다. 실패가 폴백으로 삼켜져 화면에는 지연으로만 나타났다.
129 lines
4.0 KiB
PHP
129 lines
4.0 KiB
PHP
<?php
|
|
|
|
namespace Modules\Sirsoft\Board\Tests\Feature\Upgrade;
|
|
|
|
require_once __DIR__.'/../../ModuleTestCase.php';
|
|
|
|
use App\Extension\UpgradeContext;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Modules\Sirsoft\Board\Tests\BoardTestCase;
|
|
use Modules\Sirsoft\Board\Upgrades\Upgrade_1_1_1;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
|
|
/**
|
|
* 1.1.1 평문 게시글 비밀번호 복구 업그레이드 스텝 테스트.
|
|
*
|
|
* 배경: 1.1.1 이전 게시글 수정 경로가 본인 확인용 `password` 를 저장 데이터로 흘려
|
|
* 기존 bcrypt 해시를 평문으로 덮었다. 소스 교정은 새로 덮이는 것만 막으므로,
|
|
* 이미 평문이 된 행은 이 백필이 없으면 작성자가 영구히 자기 글을 다룰 수 없다.
|
|
*
|
|
* 검증 목적:
|
|
* - 평문으로 남은 행이 bcrypt 해시로 교체된다
|
|
* - 교체 후에도 작성자가 알던 원래 비밀번호로 검증된다
|
|
* - 이미 해시인 행은 값이 그대로다 (재해싱하지 않는다)
|
|
* - 재실행해도 결과가 동일하다 (멱등)
|
|
*
|
|
* @group board
|
|
* @group upgrade
|
|
*/
|
|
class PlaintextPostPasswordRehashTest extends BoardTestCase
|
|
{
|
|
protected function getTestBoardSlug(): string
|
|
{
|
|
return 'plaintext-password-rehash';
|
|
}
|
|
|
|
/**
|
|
* 1.1.1 복구 스텝을 실행합니다.
|
|
*/
|
|
private function runRehash(): void
|
|
{
|
|
(new Upgrade_1_1_1)->run(new UpgradeContext('1.1.0', '1.1.1', '1.1.1', 'extension-upgrade'));
|
|
}
|
|
|
|
private function storedPassword(int $postId): string
|
|
{
|
|
return (string) DB::table('board_posts')->where('id', $postId)->value('password');
|
|
}
|
|
|
|
/**
|
|
* 평문으로 남은 행이 해시로 복구되고, 원래 비밀번호로 검증된다.
|
|
*
|
|
* @scenario stored=plaintext
|
|
*
|
|
* @effects plaintext_password_rehashed_to_bcrypt
|
|
*/
|
|
#[Test]
|
|
public function plaintext_rows_are_rehashed_and_still_verify(): void
|
|
{
|
|
$plain = 'legacyPlain123';
|
|
$postId = $this->createTestPost(['password' => $plain]);
|
|
|
|
$this->runRehash();
|
|
|
|
$stored = $this->storedPassword($postId);
|
|
|
|
$this->assertNotSame($plain, $stored, '평문이 그대로 남았다');
|
|
$this->assertSame('bcrypt', password_get_info($stored)['algoName'] ?? 'unknown');
|
|
$this->assertTrue(Hash::check($plain, $stored), '복구 후 원래 비밀번호로 검증되지 않는다');
|
|
}
|
|
|
|
/**
|
|
* 이미 해시인 행은 건드리지 않는다 (재해싱 금지).
|
|
*
|
|
* @scenario stored=bcrypt
|
|
*
|
|
* @effects already_hashed_rows_untouched
|
|
*/
|
|
#[Test]
|
|
public function already_hashed_rows_are_left_untouched(): void
|
|
{
|
|
$hashed = Hash::make('alreadyHashed123');
|
|
$postId = $this->createTestPost(['password' => $hashed]);
|
|
|
|
$this->runRehash();
|
|
|
|
$this->assertSame($hashed, $this->storedPassword($postId), '이미 해시인 행이 재해싱되었다');
|
|
}
|
|
|
|
/**
|
|
* 비밀번호가 없는 회원 게시글은 대상이 아니다.
|
|
*
|
|
* @scenario stored=null
|
|
*
|
|
* @effects rows_without_password_untouched
|
|
*/
|
|
#[Test]
|
|
public function rows_without_password_are_untouched(): void
|
|
{
|
|
$postId = $this->createTestPost(['password' => null]);
|
|
|
|
$this->runRehash();
|
|
|
|
$this->assertNull(DB::table('board_posts')->where('id', $postId)->value('password'));
|
|
}
|
|
|
|
/**
|
|
* 재실행해도 결과가 변하지 않는다 (멱등).
|
|
*
|
|
* @scenario run=twice
|
|
*
|
|
* @effects rehash_is_idempotent
|
|
*/
|
|
#[Test]
|
|
public function rerunning_does_not_change_the_result(): void
|
|
{
|
|
$plain = 'idempotent123';
|
|
$postId = $this->createTestPost(['password' => $plain]);
|
|
|
|
$this->runRehash();
|
|
$afterFirst = $this->storedPassword($postId);
|
|
|
|
$this->runRehash();
|
|
|
|
$this->assertSame($afterFirst, $this->storedPassword($postId), '재실행이 값을 바꿨다');
|
|
$this->assertTrue(Hash::check($plain, $this->storedPassword($postId)));
|
|
}
|
|
}
|