← All reports

[5] Frame-tree ancestor-walk consolidation (`FrameAncestorIterator`)

LowWebCore frame treeOther

0ae01ce

Low — exploit 가능한 결함은 여기서 확인되지 않습니다. 직접 구현되어 있던 ancestor walk 24곳을 동작 변경 없이 하나로 통합한 commit입니다. 의미는 앞으로 작성될 코드 쪽에 있습니다. 한 가지 기억해 둘 지점은 type 필터링된 walk가 RemoteFrame ancestor에서 fail-open되는 위치입니다.

WebCore의 frame tree는 browsing context 계층을 각 process의 관점에서 표현합니다. 같은 process 안의 frame은 LocalFrame입니다. 반면 다른 process에 있는 frame은 RemoteFrame placeholder로 나타납니다. 상당수의 보안 판단은 frame의 ancestor를 거슬러 올라가며 이루어집니다. CSP frame-ancestors, X-Frame-Options, Document::canAccessAncestor, secure context 상속, WebAuthn relying party 범위 지정이 여기에 해당합니다. 이번 변경 전에는 이런 walk가 호출 지점마다 따로 구현되어 있었습니다. 그래서 시작 노드, null 처리, LocalFrame이 아닌 ancestor의 처리 방식을 각 구현이 독자적으로 정하고 있었습니다.

관전 포인트: 점검 관점에서 의미가 있는 변경입니다. type 필터링된 policy walk가 RemoteFrame ancestor를 건너뛰면, 해당 ancestor는 origin을 하나도 제공하지 않습니다. 따라서 deny를 유발할 수도 없습니다. 즉 site isolation 환경에서는 remote ancestor를 만났을 때 실패하지 않고 건너뛰는 walk가 fail-open 방향에 해당합니다.

diff에서는 FrameAncestorIterator/FrameAncestorRange가 새로 도입되었고, ancestor walk 24곳이 이를 사용하도록 변경되었습니다. commit은 구체적인 유발 결함을 적시한 메시지는 없이 통합 자체만 기록되어 있습니다.

Source/WebCore/page/Frame.h

+ template<typename FrameType> class FrameAncestorIterator { ... };
+ template<typename FrameType = Frame> FrameAncestorRange<FrameType> ancestorFrames(Frame&);

Source/WebCore/page/csp/ContentSecurityPolicyDirectiveList.cpp

- for (Frame* ancestor = frame.tree().parent(); ancestor; ancestor = ancestor->tree().parent()) { ... }
+ for (auto& ancestor : ancestorFrames<LocalFrame>(frame)) { ... }

diff에서 FrameAncestorIterator/FrameAncestorRange가 추가되었고, 직접 구현된 ancestor walk 24곳이 이를 사용하도록 변환되었습니다. 대상에는 위의 cross-origin policy 지점을 비롯해 loader, focus/fullscreen/user gesture 처리 경로, accessibility, 그리고 WebProcess 쪽 호출 지점 두 곳이 포함됩니다. 먼저 FrameAncestorRange는 소유권을 갖는 RefPtr<Frame> m_first를 보관합니다. 덕분에 임시 range를 std::ranges::any_of에 전달해도 시작 frame이 해제되지 않고 유지됩니다. 한편 FrameLoader::allAncestorsAreComplete/started에서는 기존의 raw Frame* loop 변수가 iterator의 RefPtr<FrameType> m_current로 대체되었습니다. 이 두 함수는 FrameLoader.h에서 NODELETE annotation이 제거된 함수이기도 합니다.

생략에 의한 fail-open: 보안에 민감한 순회에서 type 필터링된 ancestor walk가 out-of-process ancestor를 조용히 건너뛰는 패턴.

Local frame과 remote frame. site isolation 환경에서 cross-origin subframe은 다른 process에 존재합니다. 로컬에서는 process 내 origin 정보가 없는 RemoteFrame placeholder로만 나타납니다. 반면 same-site frame은 LocalFrame으로 존재합니다.

Walk가 중요한 이유. 일부 ancestor walk는 그 자체가 cross-origin policy 판단입니다. CSP frame-ancestors, XFO, canAccessAncestor, secure context 상속, WebAuthn RP 범위 지정이 이에 해당합니다.

firstOfType()의 semantics. 새 primitive의 firstOfType()은 type이 맞지 않는 ancestor를 건너뛰는 것을 기본 동작으로 둡니다. rendering이나 accessibility 용도의 walk라면 건너뛰는 동작이 올바릅니다.

여기서는 memory safety 결함이나 logic 결함을 특정하지 않습니다. 각 hunk를 이전 코드와 비교해 보면, 24곳 모두에서 시작 노드가 변경 전후로 동일합니다. LocalFrame이 아닌 ancestor를 건너뛸지 실패로 처리할지도 마찬가지로 동일합니다. 따라서 보안에 민감한 traversal 코드를 동작 보존 상태로 통합한 변경으로 읽힙니다. 이와 다른 동기를 알리는 commit message는 없습니다.

그럼에도 짚어 둘 만한 메커니즘이 있습니다. site isolation 환경의 policy walk에서 RemoteFrame ancestor를 건너뛰는 동작은 fail-open에 해당합니다. 해당 ancestor는 다른 process에 있어 origin을 제공하지 않고, 결국 deny를 유발할 수 없기 때문입니다. diff에는 두 가지 형태가 나란히 드러납니다. 먼저 checkFrameAncestors는 ancestorFrames<LocalFrame>(frame)을 사용하므로 remote ancestor를 건너뜁니다. 두 번째 overload인 checkFrameAncestors(..., const Vector<Ref<SecurityOrigin>>& ancestorOrigins)는 명시적으로 전달된 ancestor origin 목록을 대상으로 같은 directive를 평가합니다 — 그 경로가 type 필터링된 walk가 건너뛴 remote ancestor를 보완하는지는 open audit 사항이고, 어느 호출자가 어느 형태를 쓰는지 확인하는 것이 첫 단계입니다. 반대로 shouldInterruptLoadForXFrameOptions는 type을 지정하지 않은 ancestorFrames(...)에 명시적인 !localFrame -> return true를 결합합니다. 이쪽이 fail-closed 형태입니다.