Search

Chromium IndexedDB Use-After-Free CVE-2025-11460 취약점 분석

태그
PurpleHound
Chrome
Property
155.png
작성날짜
2026/01/27

1. 개요

IndexedDB에서 발생한 Use-After-Free 취약점에 의한 Remote Code Execution 취약점

2. 사전지식

2.1. Blink (Renderer Process)

Blink는 Chromium에서 사용되는 랜더링 엔진이다. 웹 페이지의 HTML, CSS, JS를 화면에 그려주는 역할을 하고, 주로 DOM 트리 구축, 레이아웃 처리, 레이어 페인팅 등을하며 Renderer Process에 Sandboxed 되어 실행된다. Blink API를 사용하기 위해서는 V8에서 해당 API를 먼저 처리하게 되는데, 대부분 Mojo IPC 를 통해 Renderer Process와 Browser Process가 통신한다.
[사진 1] Blink 렌더링 파이프라인 구조

2.2. IndexedDB

IndexedDB는 단순하게 크로미움에서 오프라인에서도 데이터를 사용하기 위해 저장하는 데이터베이스다. 해당 API를 사용하기 위해서는 Blink에서 JS API를 처리해야하며, 해당 API를 통해 Storage Service를 사용할 수 있다. (Mojo IPC) 129 버전 이전까지는 IndexedDB가 컴퓨터의 디스크에 값을 저장했으나, 이후부터는 Browser Process에서 메모리 상에 저장하도록 수정되었다.

2.2.1 connection

Renderer Process에서 IndexedDB 생성을 요청할 때 IndexedDB.open() 메소드를 통해 Browser Process에 Database를 생성한다. 그러나 이후 동일 이름의 Database에 대해 삭제되지 않았으나 open() 요청이 오는 경우, 새로운 Database를 생성하는 것이 아닌 connection 객체를 생성한다.

3. 취약점 근본 원인 분석

3.1. ForceCloseAndRunTasks 함수 분석

Status Database::ForceCloseAndRunTasks(const std::string& message) { if (!bucket_context_->ShouldUseSqlite()) { DCHECK(!force_closing_); } else if (force_closing_) { // Re-entrancy can validly occur if there's an error in the code below, // e.g. in `CloseAndReportForceClose`. return Status::OK(); } force_closing_ = true; // set force_closing_ true for (Connection* connection : connections_) { connection->CloseAndReportForceClose(message); // destroy connection (free) } connections_.clear(); // clear active connections list IDB_RETURN_IF_ERROR(connection_coordinator_.PruneTasksForForceClose(message)); connection_coordinator_.OnNoConnections(); // Execute any pending tasks in the connection coordinator. ConnectionCoordinator::ExecuteTaskResult task_state; Status status; do { std::tie(task_state, status) = connection_coordinator_.ExecuteTask(false); DCHECK(task_state != ConnectionCoordinator::ExecuteTaskResult::kPendingAsyncWork) << "There are no more connections, so all tasks should be able to " "complete synchronously."; } while (task_state != ConnectionCoordinator::ExecuteTaskResult::kDone && task_state != ConnectionCoordinator::ExecuteTaskResult::kError); DCHECK(connections_.empty()); bucket_context_->QueueRunTasks(); // Queue BucketContext::RunTasks (destroy connection) return status; }
C++
복사
[코드 1] Database::ForceCloseAndRunTasks() - force_closing_ 플래그 설정 및 Connection 해제
임의의 IndexedDB database를 IndexedDB.open() 으로 생성을 한 후 제거하기 위해 IndexedDB.deleteDatabase(force_close = true) 메소드를 호출하게 되면 마지막에 Database::ForceCloseAndRunTasks 함수를 호출한다.

3.2. AbortTransactionsAndClose + ConnectionClosed 함수 분석

Connection::~Connection() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); is_shutting_down_ = true; if (!IsConnected()) { return; } AbortTransactionsAndClose(CloseErrorHandling::kAbortAllReturnLastError, "The connection is destroyed."); } std::unique_ptr<DatabaseCallbacks> Connection::AbortTransactionsAndClose( CloseErrorHandling error_handling, const std::string& message) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); if (!IsConnected()) { return {}; } //... std::unique_ptr<DatabaseCallbacks> callbacks = std::move(callbacks_); std::move(on_close_).Run(this); // on_close_ callback -> call ConnectionClosed for (auto& remotes : client_keep_active_remotes_) { remotes.reset(); } bucket_context_handle_->quota_manager()->NotifyBucketAccessed( bucket_context_handle_->bucket_locator(), base::Time::Now()); if (!status.ok()) { bucket_context_handle_->OnDatabaseError(database_.get(), status, {}); } bucket_context_handle_.Release(); return callbacks; } void Database::ConnectionClosed(Connection* connection) { TRACE_EVENT0("IndexedDB", "Database::ConnectionClosed"); // Ignore connection closes during force close to prevent re-entry. if (force_closing_) { return; } connections_.erase(connection); connection_coordinator_.OnConnectionClosed(connection); if (connections_.empty()) { connection_coordinator_.OnNoConnections(); } if (CanBeDestroyed()) { bucket_context_->QueueRunTasks(); } }
C++
복사
[코드 2] Connection 소멸 및 ConnectionClosed 콜백 처리
위 과정을 통해 connection 객체를 destroy하면 AbortTransactionsAndClose 함수를 호출한다. AbortTransactionsAndClose 함수에서는 on_close_ callback으로 Database::ConnectionClosed 함수를 호출한다. 여기서 force_closing_이 true인 경우에 connections를 database에서 지우지 않는다. 처음 과정에서 예약한 RunTask는 Database를 하나하나 Clean하는 작업을 하는데, 각 데이터베이스에 CanBeDestroyed 함수를 통해 파괴 가능 여부를 확인한 후 Database를 파괴한다.

3.3. BucketContext::RunTasks 함수 분석

void BucketContext::RunTasks() { task_run_queued_ = false; for (auto db_it = databases_.begin(); db_it != databases_.end();) { Database& db = *db_it->second; Status status = db.RunTasks(); if (!status.ok()) { OnDatabaseError(&db, status, {}); return; } if (db.CanBeDestroyed()) { db_it = databases_.erase(db_it); } else { ++db_it; } } if (CanClose() && closing_stage_ == ClosingState::kClosed) { ResetBackingStore(); } }
C++
복사
[코드 3] BucketContext::RunTasks() - Database 정리 작업 수행
위에서 force_closing_ 이 true로 세팅이 되고 다시 Database를 open하게 되면 여러 connections들이 새로 생긴다. 문제는 open한 database를 close하는 과정에서 발생한다.

3.4. 취약점 발생 지점 분석

void Database::ConnectionClosed(Connection* connection) { TRACE_EVENT0("IndexedDB", "Database::ConnectionClosed"); // Ignore connection closes during force close to prevent re-entry. if (force_closing_) { return; } connections_.erase(connection); connection_coordinator_.OnConnectionClosed(connection); if (connections_.empty()) { connection_coordinator_.OnNoConnections(); } if (CanBeDestroyed()) { bucket_context_->QueueRunTasks(); } }
C++
복사
[코드 4] Database::ConnectionClosed() - force_closing_ 조건으로 인한 조기 리턴
위 함수는 database.close 함수 호출 시 호출되는 Database::ConnectionClosed 함수다. 해당 함수는 Database에 연결된 모든 connection 객체의 포인터 집합인 connections_ 에서 지우려는 connection을 지우고 Destroy 가능하다면 RunTasks를 Queue에 넣어 삭제하는 과정을 거치는데, 문제는 위에서 force_closing_ 변수가 true이므로 아래의 모든 과정이 진행되지 않는다.
이 때 발생하는 문제점은, connections_에는 free된 객체 주소(Dangling Pointer)가 남아있으나 각 주소에 해당하는 connection 객체들은 이미 free 되어있어 해당 지점에서 쉽게 사용 가능한 UAF가 발생한다.

4. Exploit 과정

전제가 두 가지 존재한다. 해당 취약점은 Chrome Sandbox Bypass 취약점으로, 기본적으로 R/W primitives가 존재하며 힙 주소가 고정된다는 가정 하에 진행된다.
위와 같이 Dangling Pointer들을 생성하고 나서 각 포인터를 사용해야 하는데, 해당 포인터는 마찬가지로 Database::RunTasks 에서 활용된다.

4.1. Dangling Pointer가 있는 connections_ 순회

for (Connection* connection : connections_) { std::vector<int64_t> txns_to_remove; for (const auto& id_txn_pair : connection->transactions()) { Transaction* txn = id_txn_pair.second.get(); // get Transaction // Determine if the transaction's task queue should be processed. switch (txn->state()) { case Transaction::FINISHED: if (txn->mode() == blink::mojom::IDBTransactionMode::VersionChange) { finished_upgrade_transaction = txn; upgrade_transaction_commmitted = !txn->aborted(); } txns_to_remove.push_back(id_txn_pair.first); continue; case Transaction::CREATED: continue; case Transaction::STARTED: case Transaction::COMMITTING: break; } // Process the queue for transactions that are STARTED or COMMITTING. // Add transactions that can be removed to a queue. StatusOr<Transaction::RunTasksResult> task_result = txn->RunTasks();
C++
복사
[코드 5] UAF 트리거: Database::RunTasks()에서 해제된 Connection 포인터 접근
RunTasks 함수에서는 위와 같이 connections_에 존재하는 connection 포인터들을 순회하는데, 해당포인터에서는 각각의 Transaction을 받아온다. 해당 Transaction으로 RunTasks 함수를 호출하는데, 해당 코드는 아래와 같이 구현되어 있다.

4.2. DoPendingCommit 호출 조건

StatusOr<Transaction::RunTasksResult> Transaction::RunTasks() { ... // If there are no pending tasks, we haven't already committed/aborted, // and the front-end requested a commit, it is now safe to do so. if (!HasPendingTasks() && state_ == STARTED && is_commit_pending_) { processing_event_queue_ = false; Status result = DoPendingCommit(); if (!result.ok()) { // This can delete |this|. return base::unexpected(result); }; } ... }
C++
복사
[코드 6] 호출 체인 1단계: Transaction::RunTasks() → DoPendingCommit()
위 함수에서는 is_commit_pending_ 체크 후 DoPendingCommit 함수를 호출한다. 해당 함수는 아래와 같다.

4.3. DoPendingCommit에서 CancelTask까지의 호출 체인

Status Transaction::DoPendingCommit() { TRACE_EVENT1("IndexedDB", "Transaction::DoPendingCommit", "txn.id", id()); ResetTimeoutTimer(); ... } void Transaction::ResetTimeoutTimer() { timeout_timer_.Stop(); timeout_strikes_ = 0; } void DelayedTaskHandle::CancelTask() { // The delegate is responsible for cancelling the task. if (delegate_) { delegate_->CancelTask(); DCHECK(!delegate_->IsValid()); delegate_.reset(); } }
C++
복사
[코드 7] 호출 체인 2단계: DoPendingCommit() → ResetTimeoutTimer() → CancelTask()
DoPendingCommit 함수는 ResetTimeoutTimer 함수를 호출하고, 해당 함수는 Stop 메소드를 호출하는데 해당 함수 구현을 따라가보면 CancelTask 함수를 호출하는 것을 알 수 있다. 이 때 CancelTask는 delegate_ 객체를 확인하여 존재하면 CancelTask() 메소드를 호출한다. 여기서 delegate_ 객체 구현을 확인해보면 아래와 같다.

4.4. vtable을 통한 CancelTask 호출 구조

class DelayedTaskHandleDelegate : public DelayedTaskHandle::Delegate { public: explicit DelayedTaskHandleDelegate(TaskQueueImpl* outer); DelayedTaskHandleDelegate(const DelayedTaskHandleDelegate&) = delete; DelayedTaskHandleDelegate& operator=(const DelayedTaskHandleDelegate&) = delete; ~DelayedTaskHandleDelegate() override; WeakPtr<DelayedTaskHandleDelegate> AsWeakPtr(); // DelayedTaskHandle::Delegate: bool IsValid() const override; void CancelTask() override; void SetHeapHandle(HeapHandle heap_handle); void ClearHeapHandle(); HeapHandle GetHeapHandle(); // Indicates that this task will be executed. This will invalidate the handle. void WillRunTask(); private: // The TaskQueueImpl where the task was posted. // RAW_PTR_EXCLUSION: Performance reasons (based on analysis of speedometer3). RAW_PTR_EXCLUSION TaskQueueImpl* const outer_ GUARDED_BY_CONTEXT(sequence_checker_) = nullptr; // The HeapHandle to the task, if the task is in the DelayedIncomingQueue, // invalid otherwise. HeapHandle heap_handle_ GUARDED_BY_CONTEXT(sequence_checker_); SEQUENCE_CHECKER(sequence_checker_); // Allows TaskQueueImpl to retain a weak reference to |this|. An outstanding // weak pointer indicates that the task is valid. WeakPtrFactory<DelayedTaskHandleDelegate> weak_ptr_factory_ GUARDED_BY_CONTEXT(sequence_checker_){this}; };
C++
복사
[코드 8] 제어 흐름 탈취 지점: DelayedTaskHandleDelegate vtable 구조
함수 vtable에 CancelTask가 override 되도록 구현이 되어있다. 이 말은, fake vtable에서 CancelTask 위치에 우리가 아는 주소값을 넣어두고, 해당 주소에 WinExec()와 같은 함수를 넣을 수 있다면 RCE가 가능하다.
이러한 이유로 R/W Primitives로 WinExec 주소를 알아내는 것이 1차적인 필수 요소이고, 고정적으로 할당되는 Heap 주소가 필요하다. 그러므로 해당 취약점만을 활용하여 Exploit을 하기 위해서는 추가적인 RW primitives를 얻을 수 있는 취약점을 체이닝 하거나, patch를 통해 임의로 RW primitves를 획득하며 ASLR을 해제하여 Heap 주소를 고정적으로 할당하게 한다.

5. 시연 영상

위 chrome은 위와 같은 조건들을 임의로 패치한 142.0.7426.0 버전으로, 성공적으로 계산기를 열 수 있었다.

6. 완화 및 권고사항

Product
취약점 영향 버전
Chromium
129.0.0.0 ~ 142.0.7426.0 이전
Google Chrome
해당 Chromium 버전 기반 빌드
Microsoft Edge
해당 Chromium 버전 기반 빌드
1.
즉각적인 브라우저 업데이트: Chrome, Edge 등 Chromium 기반 브라우저를 최신 버전으로 업데이트해야 한다. 자동 업데이트가 비활성화된 환경에서는 수동으로 업데이트 적용 여부를 확인해야 한다.
2.
Site Isolation 활성화 확인: Chromium의 Site Isolation 기능이 활성화되어 있는지 확인한다. 이 기능은 렌더러 프로세스 간 격리를 강화하여 Sandbox Escape 공격의 영향을 제한할 수 있다.
3.
엔터프라이즈 환경 브라우저 관리: 기업 환경에서는 브라우저 버전을 중앙에서 관리하고, 취약한 버전이 사용되지 않도록 정책을 수립해야 한다.
4.
신뢰할 수 없는 웹사이트 접근 제한: 본 취약점은 악성 웹페이지 방문을 통해 트리거될 수 있으므로, 신뢰할 수 없는 웹사이트 접근을 제한하는 것이 권장된다.

7. 결론

CVE-2025-11460 취약점은 Chromium IndexedDB의 force_closing_ 플래그 처리 로직에서 발생하는 Use-After-Free 취약점이다. Database 강제 종료 후 재오픈 시 connections_ 리스트에 Dangling Pointer가 남아있어, 이후 해당 포인터에 접근할 때 UAF가 발생한다.
본 취약점은 단독으로 완전한 익스플로잇이 어려우며, R/W primitive를 제공하는 추가 취약점과의 체이닝이 필요하다. 그러나 성공적으로 악용될 경우 Renderer Sandbox를 탈출하여 시스템 수준의 임의 코드 실행이 가능하므로, 즉시 최신 버전으로 업데이트할 것을 권고한다.
당사의 솔루션인 PurpleHound는 실전 기반 시나리오를 재구성하여 기업이 보유한 보안 장비와 시스템이 실제 공격에 얼마나 효과적으로 대응 가능한지 검증할 수 있도록 지원한다. 본 보고서에서 분석한 취약점 뿐만 아니라 다른 유형의 취약점, 악성코드 활동, 파일 시스템 조작 등 다양한 위협 시나리오를 직접 실행해보며 보안 인프라의 실효성을 객관적으로 확인할 수 있다.

8. 참고자료