Skip to content

flutter_cef security hardening: nav scheme allowlist + ad-hoc compile-gate#1

Merged
wenkaifan0720 merged 6 commits into
mainfrom
feat/nav-scheme-allowlist
Jun 9, 2026
Merged

flutter_cef security hardening: nav scheme allowlist + ad-hoc compile-gate#1
wenkaifan0720 merged 6 commits into
mainfrom
feat/nav-scheme-allowlist

Conversation

@wenkaifan0720

@wenkaifan0720 wenkaifan0720 commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Two pre-bundling security-hardening slices for flutter_cef.

1. Navigation scheme allowlist (581ac93)

CefWebView(url: 'https://example.com', allowedSchemes: {'http', 'https'})

When set, the renderer's OnBeforeBrowse cancels any navigation whose scheme
isn't in the set — gating the initial load, programmatic navigate(), in-page
link clicks, and redirects. about: is always permitted. Default (null)
preserves allow-all, so it's opt-in and non-breaking. This is the
package-level primitive a host's own URL allowlist calls into; it keeps an
untrusted page off file:/data:/chrome: when the host can drive navigation
programmatically.

Plumbing: CefWebView(allowedSchemes:) -> CefWebController.create (lowercased
CSV) -> FlutterCefPlugin -> CefWebSession --allowed-schemes= -> cef_host
g_allowed_schemes -> OnBeforeBrowse.

2. Gate ad-hoc-only CEF shortcuts behind CEF_HOST_ADHOC (a7bc775)

The mock keychain + basic password store and the Mach-port peer-validation
bypass are dev/ad-hoc conveniences that must not ship in a signed release. They
now sit behind a CEF_HOST_ADHOC compile flag (ON by default, so dev/CI
builds are byte-identical). A signed release builds -DCEF_HOST_ADHOC=OFF: real
Keychain via OSCrypt + enforced peer validation — which then require correct
inside-out Developer-ID signing of the cef_host tree. build_cef_host.sh
honors CEF_HOST_ADHOC=OFF.

Verification

  • flutter analyze clean; flutter test 94 pass (incl. 2 new allowlist
    plumbing tests).
  • cef_host rebuilt clean on both CEF_HOST_ADHOC=ON and =OFF (0 errors).
  • The example app gains an allowedSchemes: {http, https} lock + a "try
    file://" button to exercise the block live (held for a manual pass before
    this lands).

🤖 Generated with Claude Code

wenkaifan0720 and others added 2 commits June 9, 2026 14:49
Host-settable allow-list of URL schemes the page may navigate to,
enforced in the renderer's OnBeforeBrowse so it covers the initial
load, programmatic navigate(), in-page link clicks, and redirects.
`about:` is always permitted; default (null) preserves allow-all, so
this is opt-in and non-breaking.

Plumbed end-to-end: CefWebView(allowedSchemes:) -> controller.create
lowercased CSV -> FlutterCefPlugin -> CefWebSession --allowed-schemes=
argv -> cef_host g_allowed_schemes. Lets an embedder keep an untrusted
page off file:/data:/chrome:, which matters when the host can drive
navigation programmatically.

Bumps to 0.1.2. cef_host rebuilt clean (0 errors); 94 Dart tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mock keychain + basic password store and the Mach-port
peer-validation bypass are dev/ad-hoc conveniences that must not ship in
a signed release. Put all three behind a new CEF_HOST_ADHOC compile flag
(ON by default, so dev/CI builds stay byte-identical). A signed release
builds with -DCEF_HOST_ADHOC=OFF: real Keychain via OSCrypt + enforced
Mach-port peer validation, which then require correct inside-out
Developer-ID signing of the cef_host tree.

- CMakeLists: option(CEF_HOST_ADHOC ON) + target_compile_definitions.
- main.mm: #ifdef-gate the command-line switches and the setenv bypass;
  the single-process switch (not a security shortcut) stays ungated.
- build_cef_host.sh: honors CEF_HOST_ADHOC=OFF and documents it.

Verified both paths compile clean (ON and OFF, 0 errors).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wenkaifan0720 wenkaifan0720 changed the title feat: navigation scheme allowlist (CefWebView.allowedSchemes) flutter_cef security hardening: nav scheme allowlist + ad-hoc compile-gate Jun 9, 2026
wenkaifan0720 and others added 4 commits June 9, 2026 15:20
CEF implements loadHtmlString as a data: URL navigation and loadFile as a
file: one — both funnel through navigate() -> kOpNavigate -> OnBeforeBrowse.
So a `{http, https}` allowlist refused the host's own content-injection
APIs (the data:/file: load was cancelled), e.g. loadHtmlString stopped
rendering.

Give those host-trusted loads a dedicated path that bypasses the allowlist:
new kOpLoadTrusted opcode -> DoNavigateTrusted sets a one-shot
g_skip_allowlist_once immediately before LoadURL, consumed by the next
OnBeforeBrowse (set + read on the same CEF UI thread, so no race). navigate(),
the initial load, in-page clicks, and redirects stay gated. The host chose
loadHtmlString/loadFile content, so it isn't subject to the page allowlist.

Plumbed end-to-end: loadHtmlString/loadFile -> _loadTrusted -> 'loadTrusted'
channel method -> CefWebSession.loadTrusted -> kOpLoadTrusted. Dartdoc +
CHANGELOG document the exemption; the controller test now asserts they route
through loadTrusted (NOT the gated navigate). analyze clean; 94 tests pass;
cef_host rebuilt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lock the demo browser's CefWebView to allowedSchemes: {http, https} and add a
toolbar "block test" button that attempts a file:// navigation — which the
renderer refuses (the page stays put), while loadHtmlString (the IME test page)
still loads since it is host-trusted content. Exercises the allowlist + the
content-load exemption end-to-end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rame only

Adversarial audit of the allowlist found two real issues in the prior
trusted-load fix:

HIGH — the one-shot g_skip_allowlist_once was stealable. LoadURL does not
invoke OnBeforeBrowse synchronously; it enqueues the navigation and the
callback arrives as a later UI task. So the global flag stayed armed across
a gap, and a page-initiated navigation to a blocked scheme already queued
(setTimeout location change, meta-refresh, pending redirect) could have its
OnBeforeBrowse run first and consume the host's exemption — a full allowlist
bypass. Replace the bool with g_trusted_pending (a multiset of exact URLs):
DoNavigateTrusted arms the specific URL, OnBeforeBrowse exempts ONLY a
matching main-frame request and consumes that one entry. A page nav to a
different URL can't steal it; a redirect of a trusted load carries a
different URL and stays gated. Only arm when an allowlist is set (immutable
after startup) so the set doesn't accumulate when the feature is off.

MEDIUM — the gate ran on every frame, so an {http,https} allowlist would
cancel legit cross-scheme SUBframes (blob:/data: iframes, PDF/video viewers,
ad frames), breaking real pages. Gate main-frame navigations only; subframes
can't change the view's top-level origin and are already policy-constrained
by Chromium.

The popup path (OnBeforePopup) was checked and is correctly suppressed
(never auto-navigates), so it is not a hole. cef_host rebuilt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…EF_HOST_ADHOC=OFF)

Make the signed-release build production-hardened, driven by the existing
CEF_HOST_ADHOC flag so one switch flips the whole dev<->release posture.

When CEF_HOST_ADHOC=OFF (signed release):
- Chromium renderer/GPU sandbox ON: each helper subprocess initializes
  CefScopedSandboxContext (dlopens libcef_sandbox.dylib, calls
  cef_sandbox_initialize) before LoadInHelper, per cef_library_loader.h; the
  browser sets settings.no_sandbox = false. The browser process is never
  sandboxed on macOS — only the helpers, which is the canonical model.
- Codesign uses entitlements.release.plist, which omits get-task-allow
  (notarization hard-fails with it; it's a local task-port priv-esc). JIT /
  unsigned-exec-memory / disable-library-validation are kept (CEF needs them).
- (already) Mach-port bypass + mock keychain compiled out.

Default CEF_HOST_ADHOC=ON is unchanged: unsandboxed, dev entitlements with
get-task-allow — byte-identical dev/CI build, since the sandbox can't validate
without proper Developer-ID signing.

CMake passes CEF_HOST_ADHOC to the helper targets (gates process_helper.mm) and
selects the entitlements file by flag. No static lib / no dist change — the
minimal dist ships libcef_sandbox.dylib + the scoped-context wrapper.

Verified: both configs compile clean (0 errors). The OFF artifact's helper
signature carries the JIT entitlements with NO get-task-allow, links
CefScopedSandboxContext, and bundles libcef_sandbox.dylib — a signing-ready
production bundle. README security section + CHANGELOG updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wenkaifan0720 wenkaifan0720 merged commit 79a8673 into main Jun 9, 2026
1 check failed
@wenkaifan0720 wenkaifan0720 deleted the feat/nav-scheme-allowlist branch June 9, 2026 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant