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 ``; } /**