From 544c3476a8ec86ee7af6e87a2545d9690552ed0f Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Tue, 15 Sep 2026 15:09:19 -0700 Subject: [PATCH] feat(server): wrap a bare Cloud UI snippet body in a ` element. A value that already looks like markup is injected byte-for-byte, so existing full-`` close. ## Risks Low risk. The change adds a branch that only affects values that do not start with `<` — previously injected as inert text, never as a running script. Values that start with `<` keep their exact bytes. The content is trusted operator HTML, consistent with the existing contract. Roll back by reverting this commit. ## Model Used Claude — `claude-fable-5` (Fable 5), extended thinking, with tool use and code execution in Claude Code. A human author reviewed and verified the change before submission. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [ ] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- doc/cloud-ui-snippet.md | 22 +++++++++++++++++++ server/src/__tests__/cloud-ui-snippet.test.ts | 19 ++++++++++++++++ server/src/cloud-ui-snippet.ts | 22 +++++++++++++++++-- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/doc/cloud-ui-snippet.md b/doc/cloud-ui-snippet.md index e7f0db9ae1..4787f3b9e0 100644 --- a/doc/cloud-ui-snippet.md +++ b/doc/cloud-ui-snippet.md @@ -29,6 +29,28 @@ included: clearing the plain variable to blank disables injection even while a base64 value is still deployed. Everything else about the snippet is unchanged. +Base64 does not defeat every firewall. Some decode the value before matching, +so they reject a base64 snippet whose decoded bytes still contain script +markup. Deliver a bare script body (below) through one of these. + +## Bare script body + +Set the value to the script body alone — the JavaScript with no surrounding +``, which would close the wrapper early. Because the value carries no +` { })).toBe(html.replace("", `${snippet}\n`)); }); + it("wraps a bare script body that carries no markup in a `; + expect(injectCloudUiSnippet(html, { PAPERCLIP_MANAGED_CONFIG: "{}", PAPERCLIP_CLOUD_UI_SNIPPET: body })) + .toBe(html.replace("", `${wrapped}\n`)); + expect(injectCloudUiSnippet(html, { + PAPERCLIP_MANAGED_CONFIG: "{}", + PAPERCLIP_CLOUD_UI_SNIPPET_B64: Buffer.from(body, "utf-8").toString("base64"), + })).toBe(html.replace("", `${wrapped}\n`)); + }); + + it("preserves literal replacement tokens when wrapping a bare script body", () => { + const body = 'console.log("$&", "$`", "$\'");'; + const result = injectCloudUiSnippet(html, { + PAPERCLIP_MANAGED_CONFIG: "{}", PAPERCLIP_CLOUD_UI_SNIPPET: body, + }); + expect(result).toContain(``); + }); + it("prefers the plain snippet when both variables are set", () => { const other = Buffer.from("", "utf-8").toString("base64"); const result = injectCloudUiSnippet(html, { diff --git a/server/src/cloud-ui-snippet.ts b/server/src/cloud-ui-snippet.ts index 65fdb9d8a7..2d3e39454f 100644 --- a/server/src/cloud-ui-snippet.ts +++ b/server/src/cloud-ui-snippet.ts @@ -17,11 +17,29 @@ export function injectCloudUiSnippet(html: string, env: CloudInstanceEnv = proce */ function resolveCloudUiSnippet(env: CloudInstanceEnv): string | null { const plain = env.PAPERCLIP_CLOUD_UI_SNIPPET; - if (plain !== undefined) return plain.trim() ? plain : null; + if (plain !== undefined) return asInjectableMarkup(plain); const encoded = env.PAPERCLIP_CLOUD_UI_SNIPPET_B64?.replace(/\s+/g, ""); if (!encoded) return null; const decoded = decodeBase64(encoded); - return decoded?.trim() ? decoded : null; + return decoded !== null ? asInjectableMarkup(decoded) : null; +} + +/** + * A configured value that already looks like markup (it starts with `<`) is + * injected verbatim, preserving the original bytes. A value that does not is + * treated as a bare script body and wrapped in a ``; } /**