From b71fd15fedef3e82530a33773656e879df42deba Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 27 Oct 2026 07:36:09 -0700 Subject: [PATCH 1/2] test(cef): deny artifact documents page file authority Regression command for hosted runner: scripts/cmux-next/test-cmux-page-path-cpp.sh. The first two new cases are statically red on the base because PageRequestAllowed accepts the artifact URL as the same page host. Native test execution intentionally deferred by task scope. Agent-Session: https://cmux.com/deeplink/agent?id=aid-ecd03ea7c2f13c412b34 --- Packages/macOS/CmuxNext/CEFShim/tests/cmux_page_path_test.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Packages/macOS/CmuxNext/CEFShim/tests/cmux_page_path_test.cpp b/Packages/macOS/CmuxNext/CEFShim/tests/cmux_page_path_test.cpp index 6acfcc3a3cd9..53426a485d28 100644 --- a/Packages/macOS/CmuxNext/CEFShim/tests/cmux_page_path_test.cpp +++ b/Packages/macOS/CmuxNext/CEFShim/tests/cmux_page_path_test.cpp @@ -84,6 +84,10 @@ int main() { // A child frame being navigated: its parent decides. Allowed(false, "about:blank", "cmux-page://cmux.agent/", "cmux.agent", true); Allowed(false, "about:blank", "https://evil.test/", "cmux.agent", false); + // Artifact documents must never inherit the page host's file authority. + Allowed(false, "cmux-page://cmux.agent/__artifact", "cmux-page://cmux.agent/", "cmux.agent", false); + Allowed(false, "about:blank", "cmux-page://cmux.agent/__artifact", "cmux.agent", false); + Allowed(false, "about:srcdoc", "cmux-page://cmux.agent/", "cmux.agent", false); { auto headers = cmux_shim::PageResponseHeaders("default-src 'self'"); bool xfo = false, corp = false, nosniff = false, csp = false; From a47a65d01ca5b1a4e8168c520164ec37d15988d4 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 27 Oct 2026 07:38:40 -0700 Subject: [PATCH 2/2] fix(cef): isolate offline artifact documents on the shared page host Serve exact cmux-page://cmux.agent/__artifact with an opaque sandbox CSP and the one-shot render protocol. Gate artifact/srcdoc navigation before the main-frame fast return, deny network/file/page subresources throughout descendant frames, and retain normal page navigation. Observed: scoped verify-local checks passed (localization-defaults, package-groups, feature-flags), git diff --check passed, bootstrap node --check passed. Not run: native CEF compilation, browser probe, and C++ tests, per task scope. Hosted regression: scripts/cmux-next/test-cmux-page-path-cpp.sh. Test-first parent b71fd15fede contains two statically failing page-authority cases; hosted red/green still required. Agent-Session: https://cmux.com/deeplink/agent?id=aid-ecd03ea7c2f13c412b34 --- .../CmuxNext/CEFShim/src/artifact_page.h | 79 +++++++++++++++++++ .../CmuxNext/CEFShim/src/cmux_page_path.h | 3 + .../macOS/CmuxNext/CEFShim/src/shim_client.mm | 22 ++++++ .../CmuxNext/CEFShim/src/shim_page_scheme.mm | 15 ++++ .../CEFShim/tests/cmux_page_path_test.cpp | 41 ++++++++++ 5 files changed, 160 insertions(+) create mode 100644 Packages/macOS/CmuxNext/CEFShim/src/artifact_page.h diff --git a/Packages/macOS/CmuxNext/CEFShim/src/artifact_page.h b/Packages/macOS/CmuxNext/CEFShim/src/artifact_page.h new file mode 100644 index 000000000000..11ebe2c6ecfc --- /dev/null +++ b/Packages/macOS/CmuxNext/CEFShim/src/artifact_page.h @@ -0,0 +1,79 @@ +// Offline artifact documents for the shared CEF agent page. No CEF dependency; +// exercised by test-cmux-page-path-cpp.sh alongside the scheme's path policy. +#ifndef CMUX_SHIM_ARTIFACT_PAGE_H_ +#define CMUX_SHIM_ARTIFACT_PAGE_H_ + +#include + +namespace cmux_shim { +inline constexpr char kArtifactFrameURL[] = "cmux-page://cmux.agent/__artifact"; +inline constexpr char kArtifactCSP[] = + "sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; " + "img-src data: blob:; media-src data: blob:; font-src data:; connect-src 'none'; " + "frame-src 'none'; worker-src 'none'; form-action 'none'; base-uri 'none'; " + "frame-ancestors cmux-page://cmux.agent"; + +inline bool IsArtifactFrameURL(const std::string& url) { + return url.substr(0, url.find_first_of("?#")) == kArtifactFrameURL; +} + +inline bool IsAgentPageURL(const std::string& url) { + const std::string origin = "cmux-page://cmux.agent"; + return url.compare(0, origin.size(), origin) == 0 && + (url.size() == origin.size() || url[origin.size()] == '/' || url[origin.size()] == '?' || + url[origin.size()] == '#'); +} + +inline bool ArtifactContextRestricted(const std::string& frame, const std::string& parent) { + return IsArtifactFrameURL(frame) || IsArtifactFrameURL(parent) || + (frame == "about:srcdoc" && IsAgentPageURL(parent)); +} + +// Only an empty, direct child of the agent page may load the bootstrap once. +// After commit, self navigation (including refresh), children and srcdoc +// replacement are denied, regardless of the destination's scheme. +inline bool ArtifactNavigationRefused(bool main, const std::string& frame, const std::string& parent, + const std::string& target) { + if (ArtifactContextRestricted(frame, parent)) return true; + if (IsArtifactFrameURL(target)) { + return main || target != kArtifactFrameURL || !IsAgentPageURL(parent) || + (!frame.empty() && frame != "about:blank"); + } + return false; +} + +inline bool ArtifactResourceAllowed(const std::string& url, bool image_media_or_font) { + return image_media_or_font && (url.rfind("data:", 0) == 0 || url.rfind("blob:", 0) == 0); +} + +// Same one-shot protocol as AgentPaneRenderFrame. The response's sandbox +// makes the origin opaque even if an embedder omits its iframe sandbox. A +// passive preview remains srcdoc with sandbox=""; only Run grants scripts. +inline char kArtifactDocument[] = R"HTML( +)HTML"; +} // namespace cmux_shim +#endif // CMUX_SHIM_ARTIFACT_PAGE_H_ diff --git a/Packages/macOS/CmuxNext/CEFShim/src/cmux_page_path.h b/Packages/macOS/CmuxNext/CEFShim/src/cmux_page_path.h index c7647123d201..6fbd5d143831 100644 --- a/Packages/macOS/CmuxNext/CEFShim/src/cmux_page_path.h +++ b/Packages/macOS/CmuxNext/CEFShim/src/cmux_page_path.h @@ -13,6 +13,8 @@ #include #include +#include "artifact_page.h" + namespace cmux_shim { inline constexpr char kCmuxPageScheme[] = "cmux-page"; @@ -201,6 +203,7 @@ inline bool GrantsCmuxPageLocalNetwork(const std::string& requesting_origin, uin inline bool PageRequestAllowed(bool main_frame, const std::string& frame_url, const std::string& parent_url, const std::string& domain) { if (main_frame) return true; + if (ArtifactContextRestricted(frame_url, parent_url)) return false; const std::string origin = std::string(kCmuxPageScheme) + "://" + domain; auto same = [&](const std::string& url) { return url.size() >= origin.size() && url.compare(0, origin.size(), origin) == 0 && diff --git a/Packages/macOS/CmuxNext/CEFShim/src/shim_client.mm b/Packages/macOS/CmuxNext/CEFShim/src/shim_client.mm index 35f3beb21875..095ac9940087 100644 --- a/Packages/macOS/CmuxNext/CEFShim/src/shim_client.mm +++ b/Packages/macOS/CmuxNext/CEFShim/src/shim_client.mm @@ -9,6 +9,7 @@ #include "include/cef_parser.h" #include "include/cef_resource_request_handler.h" #include "command_line_switches.h" +#include "artifact_page.h" #include "external_url.h" #include "local_file_handoff.h" #include "local_file_scope.h" @@ -165,6 +166,17 @@ cef_return_value_t OnBeforeResourceLoad(CefRefPtr, CefRefPtr frame) { + while (frame) { + CefRefPtr parent = frame->GetParent(); + if (ArtifactContextRestricted(frame->GetURL().ToString(), parent ? parent->GetURL().ToString() : "")) return true; + frame = parent; + } + return false; +} + class Client : public CefClient, public CefDisplayHandler, public CefLoadHandler, @@ -282,6 +294,10 @@ bool OnChromeCommand(CefRefPtr browser, int command_id, cef_window_o // tab in the other store with the same URL. bool OnBeforeBrowse(CefRefPtr browser, CefRefPtr frame, CefRefPtr request, bool user_gesture, bool is_redirect) override { + CefRefPtr parent = frame->GetParent(); + if (RestrictedArtifactFrame(frame) || + ArtifactNavigationRefused(frame->IsMain(), frame->GetURL().ToString(), + parent ? parent->GetURL().ToString() : "", request->GetURL().ToString())) return true; if (!frame->IsMain()) return false; int id = browser->GetIdentifier(); std::string url = request->GetURL().ToString(); @@ -311,6 +327,12 @@ bool OnBeforeBrowse(CefRefPtr browser, CefRefPtr frame, Ce CefRefPtr request, bool, bool, const CefString&, bool&) override { const std::string url = request->GetURL().ToString(); + const auto type = request->GetResourceType(); + if (RestrictedArtifactFrame(frame) && + !ArtifactResourceAllowed(url, type == RT_IMAGE || type == RT_MEDIA || type == RT_FONT_RESOURCE)) { + static CefRefPtr refusal = new LocalFileRefusal(); + return refusal; + } // A request with no frame (a worker, a CefURLRequest) has no folder to // judge it by: its file: loads are refused. if (LocalFileSchemeURL(url) && request->GetResourceType() != RT_MAIN_FRAME && diff --git a/Packages/macOS/CmuxNext/CEFShim/src/shim_page_scheme.mm b/Packages/macOS/CmuxNext/CEFShim/src/shim_page_scheme.mm index 2061ce638b41..73e2449baa7e 100644 --- a/Packages/macOS/CmuxNext/CEFShim/src/shim_page_scheme.mm +++ b/Packages/macOS/CmuxNext/CEFShim/src/shim_page_scheme.mm @@ -167,6 +167,21 @@ int NextToken() { frame_url = frame->GetURL().ToString(); if (CefRefPtr parent = frame->GetParent()) parent_url = parent->GetURL().ToString(); } + const std::string url = request->GetURL().ToString(); + if (IsArtifactFrameURL(url)) { + if (domain_ != "cmux.agent" || request->GetMethod().ToString() != "GET" || + request->GetResourceType() != RT_SUB_FRAME || + ArtifactNavigationRefused(main_frame, frame_url, parent_url, url)) { + static char kBody[] = "Forbidden"; + return Respond(403, "Forbidden", "text/plain", Headers(), kBody, sizeof(kBody) - 1); + } + CefResponse::HeaderMap headers; + headers.emplace("Content-Security-Policy", kArtifactCSP); + headers.emplace("Cache-Control", "no-store"); + headers.emplace("X-Content-Type-Options", "nosniff"); + headers.emplace("Referrer-Policy", "no-referrer"); + return Respond(200, "OK", "text/html", headers, kArtifactDocument, sizeof(kArtifactDocument) - 1); + } if (!PageRequestAllowed(main_frame, frame_url, parent_url, domain_)) { static char kBody[] = "Forbidden"; return Respond(403, "Forbidden", "text/plain", Headers(), kBody, sizeof(kBody) - 1); diff --git a/Packages/macOS/CmuxNext/CEFShim/tests/cmux_page_path_test.cpp b/Packages/macOS/CmuxNext/CEFShim/tests/cmux_page_path_test.cpp index 53426a485d28..311e49171412 100644 --- a/Packages/macOS/CmuxNext/CEFShim/tests/cmux_page_path_test.cpp +++ b/Packages/macOS/CmuxNext/CEFShim/tests/cmux_page_path_test.cpp @@ -88,6 +88,47 @@ int main() { Allowed(false, "cmux-page://cmux.agent/__artifact", "cmux-page://cmux.agent/", "cmux.agent", false); Allowed(false, "about:blank", "cmux-page://cmux.agent/__artifact", "cmux.agent", false); Allowed(false, "about:srcdoc", "cmux-page://cmux.agent/", "cmux.agent", false); + { + const std::string page = "cmux-page://cmux.agent/"; + const std::string artifact = cmux_shim::kArtifactFrameURL; + auto navigation = [&](bool main, const std::string& current, const std::string& parent, + const std::string& target, bool refused) { + Expect(cmux_shim::ArtifactNavigationRefused(main, current, parent, target) == refused, + "artifact navigation " + current + " -> " + target + " parent " + parent); + }; + navigation(false, "about:blank", page, artifact, false); + navigation(false, "", page, artifact, false); + navigation(true, "about:blank", "", artifact, true); + for (const auto& parent : {"https://evil.test/", "cmux-page://cmux.agent.evil/", + "cmux-page://cmux.history/", "cmux-page://evil@cmux.agent/"}) { + navigation(false, "about:blank", parent, artifact, true); + } + for (const auto& target : {artifact, artifact + "?copy=1", page, std::string("about:srcdoc"), + std::string("https://example.test/"), std::string("file:///tmp/a.html"), + std::string("data:text/html,replace"), std::string("javascript:alert(1)")}) { + navigation(false, artifact, page, target, true); + navigation(false, "about:srcdoc", page, target, true); + navigation(false, "about:blank", artifact, target, true); + } + navigation(false, "about:blank", page, artifact + "?copy=1", true); + navigation(false, page, "", "https://example.test/", false); + navigation(false, "about:blank", page, "http://localhost:3000/", false); + for (const auto& url : {"https://example.test/a.png", "http://127.0.0.1:4000/api", "file:///tmp/a.png", + "cmux-page://cmux.agent/index.html", "cmux-agent://pane/index.html"}) { + Expect(!cmux_shim::ArtifactResourceAllowed(url, true), "offline artifact refuses " + std::string(url)); + } + Expect(cmux_shim::ArtifactResourceAllowed("data:image/png;base64,AA", true), "inline image allowed"); + Expect(cmux_shim::ArtifactResourceAllowed("blob:null/test", true), "inline media allowed"); + Expect(!cmux_shim::ArtifactResourceAllowed("data:text/html,nested", false), "inline document refused"); + Expect(!cmux_shim::ArtifactResourceAllowed("blob:null/script", false), "blob script refused"); + const std::string policy = cmux_shim::kArtifactCSP; + Expect(policy.find("sandbox allow-scripts;") == 0 && policy.find("allow-same-origin") == std::string::npos, + "artifact response enforces opaque origin"); + Expect(policy.find("connect-src 'none'") != std::string::npos && + policy.find("frame-src 'none'") != std::string::npos && + policy.find("form-action 'none'") != std::string::npos, + "artifact response refuses connections, children and forms"); + } { auto headers = cmux_shim::PageResponseHeaders("default-src 'self'"); bool xfo = false, corp = false, nosniff = false, csp = false;