[5] Frame-tree ancestor-walk consolidation (`FrameAncestorIterator`)
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
Source/WebCore/page/csp/ContentSecurityPolicyDirectiveList.cpp
Patch Details
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를 조용히 건너뛰는 패턴.
Background
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라면 건너뛰는 동작이 올바릅니다.
Analysis
여기서는 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 형태입니다.
Audit directions
- Policy 코드의 type 필터링된 ancestor walk.
ancestorFrames<LocalFrame>(...)을 사용하는 보안 판단이라면, remote ancestor를 반영하는 별도 경로가 반드시 있어야 합니다. origin 목록 overload나 fail-closed 기본값이 그 예입니다. CSPframe-ancestors, XFO,canAccessAncestor, WebAuthn RP 범위 지정에서RemoteFrameancestor 때문에 walk가 ancestor를 실제보다 적게 세는 상황이 가능한지 점검할 필요가 있습니다. 코드 리뷰에서는 allow/deny를 결정하는 함수 안의ancestorFrames<LocalFrame>이 단서가 됩니다.!localFrame -> return truefail-closed 패턴과 대조해 보면 차이가 드러납니다. std::rangesadaptor 아래에서의 owning range lifetime.FrameAncestorRange의 소유형m_first는std::ranges::any_of를 거치는 동안 임시 객체를 유지하기 위해 존재합니다. 새로운 호출 지점이 range adaptor를 조합하는 과정에서 이 ownership을 잃지 않는지 확인해야 합니다.