Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions Packages/macOS/CmuxNext/CEFShim/src/artifact_page.h
Original file line number Diff line number Diff line change
@@ -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 <string>

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(<!doctype html>
<html><head><meta charset="utf-8"><script>
"use strict";
(function () {
var host = parent;
function size() {
var body = document.body;
host.postMessage({type:"cmux-render-size",height:Math.ceil(Math.max(
document.documentElement.scrollHeight,body ? body.scrollHeight : 0))},"*");
}
function show(event) {
var data = event.data;
if (event.source !== host || !data || data.type !== "cmux-render" || typeof data.html !== "string") return;
removeEventListener("message",show);
document.open(); document.write(data.html); document.close();
var theme = document.createElement("style");
theme.textContent = typeof data.css === "string" ? data.css : "";
var head = document.head || document.documentElement;
head.insertBefore(theme,head.firstChild);
new ResizeObserver(size).observe(document.documentElement);
addEventListener("load",size); size();
}
addEventListener("message",show);
host.postMessage({type:"cmux-render-ready"},"*");
})();
</script></head></html>)HTML";
} // namespace cmux_shim
#endif // CMUX_SHIM_ARTIFACT_PAGE_H_
3 changes: 3 additions & 0 deletions Packages/macOS/CmuxNext/CEFShim/src/cmux_page_path.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#include <utility>
#include <vector>

#include "artifact_page.h"

namespace cmux_shim {

inline constexpr char kCmuxPageScheme[] = "cmux-page";
Expand Down Expand Up @@ -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 &&
Expand Down
22 changes: 22 additions & 0 deletions Packages/macOS/CmuxNext/CEFShim/src/shim_client.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -165,6 +166,17 @@ cef_return_value_t OnBeforeResourceLoad(CefRefPtr<CefBrowser>, CefRefPtr<CefFram
return std::string();
}

// Inspect the whole ancestry: blank/srcdoc children cannot shed the offline
// document's restrictions by inheriting an ancestor's ordinary page URL.
bool RestrictedArtifactFrame(CefRefPtr<CefFrame> frame) {
while (frame) {
CefRefPtr<CefFrame> 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,
Expand Down Expand Up @@ -282,6 +294,10 @@ bool OnChromeCommand(CefRefPtr<CefBrowser> browser, int command_id, cef_window_o
// tab in the other store with the same URL.
bool OnBeforeBrowse(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefRequest> request,
bool user_gesture, bool is_redirect) override {
CefRefPtr<CefFrame> 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();
Expand Down Expand Up @@ -311,6 +327,12 @@ bool OnBeforeBrowse(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, Ce
CefRefPtr<CefRequest> 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<CefResourceRequestHandler> 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 &&
Expand Down
15 changes: 15 additions & 0 deletions Packages/macOS/CmuxNext/CEFShim/src/shim_page_scheme.mm
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,21 @@ int NextToken() {
frame_url = frame->GetURL().ToString();
if (CefRefPtr<CefFrame> 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);
Expand Down
45 changes: 45 additions & 0 deletions Packages/macOS/CmuxNext/CEFShim/tests/cmux_page_path_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,51 @@ 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);
{
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;
Expand Down
Loading