← All reports

Escape 키로 keyboard lock이 걸린 fullscreen을 취소한다는 보장이 사라진 문제 (311660)

MediumWebCore Fullscreen API / keyboard-lock 통합LogicError

CVE: CVE-2026-64730 · Safari 26.6 · 2026년 7월 27일 릴리스 Impact: 악성 콘텐츠를 프레임으로 삽입한 웹사이트를 방문하면 UI spoofing으로 이어질 수 있음 Apple 설명: 개선된 UI로 문제를 해결함 Credit: Kagami Rosylight of Mozilla

1ab2bb4 | Bugzilla 311660

Medium — 메모리를 건드리는 문제는 아니지만, 출시된 기능이 페이지에 Escape 키를 넘겨주면서도 사용자에게 "대신 무엇을 눌러야 하는지" 알려주는 안내는 빠져 있었습니다. 이 문제가 확장될 수 있는 한계는 primitive가 아니라, exit 제스처가 고장 났다고 믿는 사용자를 노리는 phishing입니다.

Fullscreen은 페이지가 화면 전체를 요청할 수 있는 유일한 Web API이며, 그 대가로 명확한 계약을 맺습니다. 브라우저는 자신의 URL 바, 탭 스트립, 보안 표시기를 모두 숨기는 대신, 페이지가 가로챌 수 없는 탈출 수단을 사용자에게 보장합니다. Keyboard lock은 이 보장 중 절반을 의도적으로 깎아내리는 기능입니다. 브라우저 내장 터미널, 원격 데스크톱 클라이언트, 게임 등이 Escape, Meta 조합, function 키를 브라우저가 가로채지 않고 그대로 전달받을 수 있도록 하기 위해 존재합니다. 대신 엔진은 약 1.5초간 눌러야 하는 press-and-hold Escape를 페이지가 억제할 수 없는 탈출 경로로 제공하는데, 결국 이 보장은 사용자가 그 제스처의 존재를 알고 있는지에 의존하게 됩니다.

관전 포인트: 악성 콘텐츠를 프레임으로 삽입한 페이지가 화면 전체를 장악한 뒤 짧은 Escape 입력을 흡수해버리면, 사용자는 attacker가 그려낸 화면 안에 계속 머무르게 됩니다. 이 시간 동안 브라우저 자체의 UI나 다른 사이트의 로그인 페이지를 흉내 낼 여지가 생깁니다.

Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml

FullScreenKeyboardLock:
type: bool
- status: stable
+ status: unstable
category: dom
humanReadableName: "Fullscreen API based Keyboard Lock"
humanReadableDescription: "Fullscreen API based Keyboard Lock"
condition: ENABLE(FULLSCREEN_API)
defaultValue:
WebKitLegacy:
- default: true
+ default: false
WebKit:
- default: true
+ default: false
WebCore:
- default: true
+ default: false

파일 하나, preference 하나, 값 변경 6줄로 이루어진 patch입니다. FullScreenKeyboardLockstatus: stable에서 status: unstable로 되돌아가고, WebKitLegacy, WebKit, WebCore 세 consumer 모두에서 defaultValuetrue에서 false로 바뀝니다. condition: ENABLE(FULLSCREEN_API) build gate는 그대로 유지되며, human-readable name과 description 문자열도 손대지 않습니다.

이 diff에 담기지 않은 부분이 오히려 중요합니다. C++ 코드 변경이 없습니다. IDL binding 변경도 없습니다. fullscreen exit 경로, key-event routing, hold-timer 로직 어디에도 손을 대지 않았습니다. commit message는 이 점을 정확히 명시합니다. 이 patch는 플래그를 처음 켰던 303093@main 커밋의 revert입니다. keyboard-lock 구현 자체는 트리에 온전히 남아 있고, 이 patch는 단지 기본 빌드에서 도달 불가능하게 만들 뿐입니다.

  Before (303093@main):            After (this revert):
  status: stable                   status: unstable
    └─► default: true                └─► default: false
          └─► page may lock                └─► lock request inert
                └─► short Esc → page             └─► short Esc → exit fullscreen
                      (hold-to-exit UI
                       never shipped)

enablement만 켰다가 enablement만 다시 끈, 이 단순한 형태 자체가 이 문제가 구현상의 결함이 아니었음을 말해줍니다. 엔진은 원래 설계된 대로 동작했을 뿐입니다.

Fullscreen API. 페이지가 요소에 대해 requestFullscreen()을 호출하면, user-activation 요구 조건을 만족하는 한 그 요소가 화면 전체를 차지합니다. 이 상태에서는 브라우저 자신의 화면 요소, 즉 URL 바, 탭 스트립, TLS 및 permission 표시기, 윈도우 장식이 모두 가려집니다. 사용자 눈에 보이는 모든 것은 페이지가 직접 그린 화면입니다.

Keyboard lock. fullscreen 상태에서만 의미를 갖는 별도의 기능으로, 평소 브라우저나 OS가 가로채던 키 조합을 브라우저가 소비하지 않고 페이지의 key event handler로 그대로 라우팅합니다. 이 조합 중에서도 Escape가 특히 중요한데, Escape는 동시에 브라우저의 기본 fullscreen 탈출 제스처이기도 하기 때문입니다. 웹 터미널이나 스트리밍되는 원격 데스크톱 입장에서는 순수한 Escape 입력이 반대편 끝까지 전달되어야 하므로, 이를 잠그는 것이 바로 이 기능의 목적입니다.

Press-and-hold exit gesture. Escape가 잠기면 사용자에게 키보드로 빠져나갈 방법이 아예 없어지는 상황을 막기 위해, 엔진들은 약 1.5초가량 Escape를 누르고 있는 동작을 페이지가 억제할 수 없는 탈출 경로로 제공합니다. 이 제스처는 엔진 쪽에서 무조건적으로 동작하도록 설계되어 있으며, 설계상 사용자에게 "키를 누르고 있어야 한다"고 알려주는 embedder 소유의 UI 텍스트와 짝을 이루게 되어 있습니다. 계층은 둘, 소유 주체도 둘이지만, 계약은 하나입니다.

UnifiedWebPreferences.yaml. WebKit feature flag에 대한 WTF의 single source of truth입니다. Source/WTF/Scripts/Preferences/ 아래의 build script가 이 파일을 읽어, WebKit·WebKitLegacy·WebCore가 사용하는 framework별 WebPreferences accessor를 생성합니다. 여기서 중요한 필드는 두 개입니다. status는 성숙도 표시로, stable은 출시되어 기본으로 켜져 있음을, unstable은 여전히 스위치 뒤에 숨어 있음을 의미합니다. defaultValue는 consumer별 초기값을 담고 있으며, embedder는 런타임에 이 값을 자유롭게 override할 수 있습니다.

이 문제는 mitigation-completeness 실패에 해당합니다. 안전을 담보하는 두 부분짜리 계약 중 절반만 켜진 채 출시되었고, 나머지 절반은 아직 작성되지 않은 상태였습니다.

  page enters fullscreen ──► requests keyboard lock
        │                          │
        │                          ├─► (a) engine: hold-Esc 1.5s exit   [SHIPPED]
        │                          └─► (b) embedder: "hold Esc to exit" [MISSING]
        │
        ▼
  user presses Esc briefly ──► delivered to page handlers
        │                            │
        └─ nothing visibly happens   └─ attacker keeps full display
                                        (no URL bar, no indicators)

다이어그램의 두 분기를 따라가 보겠습니다. 분기 (a), 즉 엔진 쪽 hold 제스처는 구현되어 정상 동작합니다. 사용자가 Escape를 전체 구간만큼 누르고 있으면 페이지가 무엇을 하든 fullscreen을 벗어날 수 있습니다. 문제는 분기 (b)에서 invariant가 깨진다는 점입니다. commit message는 사용자에게 Escape를 누르고 있으라고 안내하는 UI 작업이 완료되지 않았다는 사실을 명확히 밝히고 있습니다. mitigation 전체의 안전성 논리가 사용자가 대신 이 동작을 알고 있다는 전제에 의존하는 이상, 그 사실을 알려주는 부분까지 함께 출시되기 전까지는 완성되었다고 볼 수 없습니다.

그 결과 사용자 입장에서는 키가 고장 난 것처럼 보이는 discoverability 실패로 이어집니다. 구체적인 흐름은 다음과 같습니다.

  1. 페이지가 fullscreen에 진입하고 keyboard lock을 요청합니다.
  2. 빠져나가고 싶은 사용자가 평소처럼 Escape를 짧게 누릅니다.
  3. 이 입력은 페이지의 key handler로 전달됩니다. fullscreen은 종료되지 않습니다.
  4. 설명 UI 자체가 존재하지 않기 때문에, 화면상에는 아무 이유도 나타나지 않습니다.
  5. 사용자는 키가 작동하지 않는다고 결론짓고, 페이지가 그리고 있는 화면을 계속 바라보게 됩니다.

5번 단계가 바로 attacker에게 열리는 시간대이며, 그 길이는 엔진이 강제하는 무언가가 아니라 사용자의 인내심에 달려 있습니다. 이 시간 동안 페이지는 브라우저 고유의 신뢰 표시기가 모두 억제된 채로 화면 전체를 소유하게 되는데, 이는 정교하게 만든 브라우저 UI 복제본이나 OS 스타일의 dialog, 다른 origin의 로그인 폼이 그럴듯하게 먹혀드는 조건과 정확히 일치합니다. Apple의 impact 항목도 도달 가능한 케이스를 명시합니다. 악성 콘텐츠를 프레임으로 삽입한 웹사이트를 방문하는 경우가 그것입니다. 사용자가 직접 방문한 최상위 페이지가 악성일 필요는 없습니다. 프레임으로 삽입된 third-party 콘텐츠도 동일한 fullscreen 화면을 상속받기 때문에, 사용자가 보고 있다고 믿는 origin과 실제로 화면을 그리고 있는 origin이 서로 다를 수 있습니다.

attacker가 이를 통해 얻는 것은 credential phishing과 false origin attribution이며, memory-corruption primitive는 아닙니다. 여기서는 process 경계를 넘는 동작이 전혀 없고, 애초에 그럴 필요도 없습니다. 이 문제는 전적으로 WebContent process 내부, fullscreen 화면을 렌더링하는 부분에 머물러 있으며, 공격 대상은 process isolation이 아니라 키보드 앞의 사람입니다. 한계도 분명히 짚어둘 필요가 있습니다. 사용자가 진짜로 갇히는 것은 아닙니다. Escape를 누르고 있는 방법은 여전히 유효하고, OS 레벨의 window management도 손대지 않은 상태입니다. 탈출 경로가 사라진 것이 아니라, 발견하기 어려워졌을 뿐입니다.

이 fix는 동작을 고치는 방식이 아니라 도달 가능성 자체를 제거하는 방식으로 invariant를 복원합니다. 세 consumer 모두 defaultValue: false로 바뀌면서, 기본 빌드에서는 페이지의 keyboard-lock 요청이 더 이상 효력을 갖지 않게 됩니다. 결과적으로 짧은 Escape 입력이 사용자가 이미 알고 있던 fullscreen 탈출 제스처로 다시 돌아갑니다. status: unstable로의 격하는 이에 맞춘 bookkeeping에 해당하며, 이 플래그가 더 이상 관련 계층 전체가 완성되었다는 주장을 하지 않도록 만듭니다. commit message에 따르면, 안내 UI가 준비되는 대로 이 enablement는 다시 reland될 예정입니다.

keyboard lock이 켜진 채로 출시되었지만, 사용자에게 Escape를 누르고 있으라고 알려줄 UI는 없었습니다. 그 결과 페이지가 exit 제스처를 흡수하고 화면 전체를 계속 소유할 수 있는 상태가 되었습니다.

이 fix의 형태를 눈여겨볼 필요가 있습니다. revert 자체가 fix의 전부입니다. keyboard-lock 구현은 트리에 그대로 남아 있으므로, 어떤 embedder나 downstream port가 FullScreenKeyboardLock을 다시 true로 돌리면 안내 UI가 여전히 빠진 채로 동일한 상태로 되돌아가게 됩니다. 여기서 true로의 non-default 설정은 지원되는 구성이 아니라, 이 문제를 다시 끌어들이는 행위로 읽어야 합니다. 좀 더 일반화하면, status: stable은 엔진·embedder UI·platform HUD가 모두 완성되었다는 주장이지만, 이를 기계적으로 검증하는 장치는 존재하지 않습니다. YAML 파일 하나의 토큰 하나가 전혀 다른 repository에 걸쳐 있는 준비 작업 전체를 대변하고 있는 셈입니다.