← All reports

Fetch dictionaries declared by rel=compression-dictionary and Link headers

Component: WebCore loader | 6248fa7

Source/WebCore/dom/Document.cpp

+void Document::queueCompressionDictionaryLoad(Function<void()>&& load)
+{
+ if (!m_loadEventFinished) {
+ m_pendingCompressionDictionaryLoads.append(WTF::move(load));
+ return;
+ }
+ eventLoop().queueTask(TaskSource::Networking, WTF::move(load));
+}
+
+void Document::flushPendingCompressionDictionaryLoads()
+{
+ ASSERT(m_loadEventFinished);
+ if (!settings().compressionDictionaryEnabled())
+ return;
+ if (RefPtr documentLoader = loader()) {
+ auto linkHeader = documentLoader->response().httpHeaderField(HTTPHeaderName::Link);
+ if (!linkHeader.isEmpty()) {
+ m_pendingCompressionDictionaryLoads.append([document = Ref { *this }, linkHeader = WTF::move(linkHeader)] {
+ LinkLoader::loadCompressionDictionariesFromHeader(linkHeader, document->url(), document);
+ });
+ }
+ }
+ for (auto& load : std::exchange(m_pendingCompressionDictionaryLoads, { }))
+ eventLoop().queueTask(TaskSource::Networking, WTF::move(load));
+}

Source/WebCore/loader/LinkLoader.cpp

+void LinkLoader::loadCompressionDictionaryLink(const LinkLoadParameters& params, Document& document)
+{
+ RefPtr client = m_client.get();
+ if (!client || !client->shouldLoadLink())
+ return;
+ document.queueCompressionDictionaryLoad([protectedThis = Ref { *this }, params, weakDocument = WeakPtr { document }] {
+ RefPtr document = weakDocument.get();
+ if (!document)
+ return;
+ auto resourceClient = loadCompressionDictionaryIfNeeded(params, *document, protectedThis.ptr());
+ ...
+ });
+}

Compression Dictionary Transport는 페이지가 "dictionary" 리소스를 등록해두면, 이후의 HTTP 응답을 그 dictionary 기준으로 압축해 전송 크기를 줄일 수 있게 해주는 기능입니다. 이번 commit은 <link rel=compression-dictionary>나 Link 헤더로 선언된 dictionary를 실제로 fetch하는 부분을 구현하며, 이 과정에서 새로운 compression-dictionary fetch destination과 CSP connect-src gating, 그리고 load 이벤트 발생 시점까지 요청을 지연시키는 Document 내부 큐잉 로직이 함께 추가되었습니다.

Fetch는 항상 CORS 요청 형태로 이뤄지며 connect-src에 의해 gating됩니다. <link> 엘리먼트로 선언된 dictionary는 critical-path 리소스와 경쟁하지 않도록 window load 이벤트 이후로 의도적으로 지연되는 반면, 서브리소스의 Link 헤더로 선언된 dictionary는 명시적으로 등록 대상에서 제외됩니다. FetchOptions::Destination에 새로 추가된 CompressionDictionary 값은 직렬화 리스트 전반에 전파되어 WebContent↔NetworkProcess 간 IPC 경계를 넘나들게 되고, NetworkLoadCheckerCachedResourceLoader의 CSP 로직도 이 값을 인식하도록 확장되었습니다. 전체 기능은 새로운 CompressionDictionaryEnabled preference 뒤에 감춰져 있으며, 테스트는 가능하지만 기본값은 off입니다.

HTMLLinkElement::process / Link header on document response
        │  rel=compression-dictionary
        ▼
  LinkLoader::loadCompressionDictionaryLink
        ▼
  Document::queueCompressionDictionaryLoad ──► (held until load event)
        ▼ flushPendingCompressionDictionaryLoads
  LinkLoader::loadCompressionDictionaryIfNeeded
        ▼
  CachedResourceLoader::allowedByContentSecurityPolicy (connect-src)
        ▼  IPC (FetchOptions with new CompressionDictionary destination)
  NetworkProcess: NetworkLoadChecker::isAllowedByContentSecurityPolicy
        ▼
  CORS fetch of dictionary resource (nothing done with body yet)

이번 변경으로, 실제 decompression 로직이 존재하기도 전에 새로운 web-exposed 기능을 위한 fetch/loader 배관이 먼저 만들어졌습니다. 그 결과 나중에 착지할 security-sensitive한 decode 경로에 앞서, 페이지 콘텐츠에서 도달 가능한 CSP-gated loader 표면이 먼저 넓어지게 됩니다.

여기서 눈여겨볼 패턴은, 정책 컨텍스트를 캡처한 시점과 실제 load가 수행되는 시점이 갈라진다는 점입니다. Load는 그 정책 컨텍스트가 캡처된 지점을 지나 지연 실행되고, 그 결과 CSP check와 fetch가 서로 다른 시점에, 어쩌면 서로 다른 상태를 기준으로 수행됩니다. 좁게 보면, 이 큐는 load 이벤트가 발생할 때까지 closure를 들고 있습니다. 여기서 매치를 가려낼 핵심 단서는, 지연된 load가 WeakPtr<Document>를 캡처하고 있는데 정작 정책 판단(allowedByContentSecurityPolicy)은 큐잉 시점이 아니라 지연 실행되는 body 안에서 이뤄진다는 점입니다. 그 사이에 CSP가 바뀌었거나 document가 detach된 경우가 바로 이런 divergence가 실제로 드러나는 케이스이기 때문입니다. 조금 더 넓게 보면, WebCore 안에서 load 이벤트로 지연되거나 event loop에 큐잉되는 다른 loader 경로들, 즉 preload scanning, <link rel=prefetch>, lazy-loaded image, speculative preconnect 등에서도 동일하게 "캡처 후 나중에 check"하는 순서가 반복되는지 점검할 필요가 있습니다. 아울러 이들의 취소 경로도 함께 살펴야 합니다. LinkLoader::~LinkLoadercancelLoad는 진행 중인 요청을 취소하고 재발행하게 만드는 crossorigin 속성 변경을, 그 진행 중인 load가 여전히 가리키고 있는 client를 해제하지 않은 채 제대로 처리해야 합니다. 가장 넓게 보면, IPC 경계를 넘나들며 여러 switch 문에 영향을 주는 타입에 enum 값을 추가하는 작업 자체가 하나의 위험 범주에 해당합니다. FetchOptions::Destination에 새로 추가된 CompressionDictionary 값이 NetworkLoadChecker, CachedResourceLoader, 직렬화 리스트의 모든 consumer를 거쳐가는 과정을 추적하면서, 거부가 아니라 조용히 허용해버리는 default: 분기가 있는지 확인해야 합니다. 이런 지점이 바로 이 구조가 만들어낼 수 있는 CSP-gating 누락 지점입니다.