← All reports

Escape key no longer guarantees cancelling fullscreen (with keyboard lock) (311660)

MediumWebCore Fullscreen API / keyboard-lock integrationLogicError

CVE: CVE-2026-64730 · Safari 26.6 · Released July 27, 2026 Impact: Visiting a website that frames malicious content may lead to UI spoofing Apple's description: The issue was addressed with improved UI. Credit: Kagami Rosylight of Mozilla

1ab2bb4 | Bugzilla 311660

Medium — no memory is touched, but the feature that shipped enabled hands a page the Escape key and withholds the notice that told the user what to press instead. The escalation ceiling is phishing against a user who believes the exit gesture is broken, not a primitive.

Fullscreen is the one Web API that lets a page ask for the entire display, and the trade it makes is explicit: the browser hides its own URL bar, tab strip, and security indicators, and in exchange guarantees the user a way out that the page cannot intercept. Keyboard lock deliberately erodes half of that guarantee — it exists so that in-browser terminals, remote-desktop clients, and games can receive Escape, Meta combinations, and function keys instead of having the browser eat them. The engine compensates by substituting a press-and-hold Escape (roughly 1.5 seconds) as the unsuppressible exit path, which means the guarantee now rests on the user knowing that gesture exists.

The angle: A page that frames hostile content can take the whole screen, swallow the short Escape press, and hold the user in an attacker-painted surface long enough to impersonate the browser's own interface or another site's login page.

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

One file, one preference, six lines of value changes. FullScreenKeyboardLock moves from status: stable back to status: unstable, and its defaultValue flips from true to false for all three consumers — WebKitLegacy, WebKit, and WebCore. The condition: ENABLE(FULLSCREEN_API) build gate is untouched, as are the human-readable name and description strings.

What the diff does not contain is the important part. No C++ changes. No IDL binding changes. Nothing in the fullscreen exit path, the key-event routing, or the hold-timer logic. The commit message names this precisely: a revert of 303093@main, the commit that flipped the flag on in the first place. The keyboard-lock implementation stays in the tree in full; the patch only makes it unreachable in a default build.

  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)

That shape — enablement-only in, enablement-only out — is what tells you this was never an implementation defect. The engine did what it was built to do.

Fullscreen API. A page calls requestFullscreen() on an element and, subject to a user-activation requirement, the element occupies the whole display. While it is active the browser withholds its own surfaces: URL bar, tab strip, TLS and permission indicators, window decorations. Everything the user can see is drawn by the page.

Keyboard lock. A separate capability, only meaningful in fullscreen, that routes normally-reserved key combinations to the page's key event handlers rather than letting the browser or the OS consume them. Escape is the interesting member of that set, because Escape is also the browser's default fullscreen-exit gesture. Locking it is the whole point for a web terminal or a streamed remote desktop, where a bare Escape must reach the far end.

Press-and-hold exit gesture. Because a locked Escape would otherwise leave the user with no keyboard exit, engines substitute a held Escape — on the order of a second and a half — as an exit path the page cannot suppress. The gesture is engine-side and unconditional; it is paired, by design, with embedder-owned user-interface text that tells the user to hold the key. Two layers, two owners, one contract.

UnifiedWebPreferences.yaml. WTF's single source of truth for WebKit feature flags. A build script under Source/WTF/Scripts/Preferences/ reads it and generates the per-framework WebPreferences accessors that WebKit, WebKitLegacy, and WebCore consume. Two fields matter here. status is the maturity label: stable means shipped and on by default, unstable means still behind a switch. defaultValue carries the per-consumer initial value; embedders remain free to override it at runtime.

This is a mitigation-completeness failure — half of a two-part safety contract shipped enabled while the other half was still unwritten.

  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)

Follow the two branches in the diagram. Branch (a) — the engine-side hold gesture — was implemented and works; a user holding Escape for the full interval leaves fullscreen no matter what the page does. Branch (b) is where the invariant breaks. The commit message states plainly that the user-interface work instructing the user to hold Escape was never completed. A mitigation whose entire safety argument is the user knows to do this instead is not shipped until the part that tells them is shipped too.

The consequence is a discoverability failure that reads to the user as a broken key. Concretely:

  1. A page enters fullscreen and requests keyboard lock.
  2. The user, wanting out, presses Escape the way they always have — a short press.
  3. The press is routed to the page's key handlers. Fullscreen does not end.
  4. Nothing on screen explains why, because the explanatory UI does not exist.
  5. The user concludes the key is not working and keeps looking at whatever the page is drawing.

Step 5 is the attacker's window, and its length is bounded by the user's patience rather than by anything the engine enforces. During it the page owns every pixel with the browser's own trust indicators suppressed — which is exactly the condition under which a convincing replica of browser UI, an OS-style dialog, or another origin's login form becomes plausible. Apple's impact line pins the reachable case: visiting a website that frames malicious content. The top-level page the user chose to visit need not be the hostile one; framed third-party content inherits the same fullscreen surface, so the origin the user believes they are looking at and the origin painting the screen can differ.

What an attacker gains from this is credential phishing and false origin attribution, not a memory-corruption primitive — nothing here crosses a process boundary, and none needs to. The whole issue lives inside the WebContent process and its rendering of the fullscreen surface; the target is the human at the keyboard, not process isolation. It is worth being precise about the ceiling too: the user is not genuinely trapped. Holding Escape still works, and OS-level window management is untouched. The exit route is undiscoverable, not absent.

The fix restores the invariant by removing reachability rather than by repairing behavior. With defaultValue: false on all three consumers, a page's keyboard-lock request no longer takes effect in a default build, so the short Escape press returns to being the fullscreen-exit gesture the user already knows. The status: unstable demotion is the matching bookkeeping: the flag stops asserting that every collaborating layer is complete. Per the commit message, the enablement is expected to reland once the instructional UI is available.

Keyboard lock shipped enabled while the UI that would have told users to hold Escape did not, leaving a page free to swallow the exit gesture and own the entire display.

Note the shape of the fix: the revert is the whole fix. The keyboard-lock implementation remains in the tree, so any embedder or downstream port that flips FullScreenKeyboardLock back on re-enters the identical state — with the instructional UI still missing. A non-default true here should be read as re-introducing the issue, not as a supported configuration. More generally, status: stable is an assertion that engine, embedder UI, and platform HUD are all finished, and nothing mechanical checks that assertion; it is one token in one YAML file standing in for readiness work that lives in other repositories entirely.