Skip to content
Open
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
44 changes: 44 additions & 0 deletions components/LiveControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { context } from "../deco.ts";
import { DomInspector, DomInspectorActivators } from "../deps.ts";
import type { Flag, Site } from "../types.ts";
import { adminDomains } from "../utils/admin.ts";

const IS_LOCALHOST = context.deploymentId === undefined;

Expand Down Expand Up @@ -110,9 +111,45 @@ const main = () => {
}
};

const STATIC_TRUSTED_ORIGINS = [
"https://deco.cx",
"https://admin.deco.cx",
"https://play.deco.cx",
"https://admin-cx.deco.page",
"https://deco.chat",
"https://admin.decocms.com",
"https://decocms.com",
"https://studio.decocms.com",
];
const runtimeTrustedOrigins = (() => {
try {
const el = document.getElementById("__DECO_TRUSTED_ORIGINS");
const parsed = JSON.parse(el?.textContent || "[]");
return Array.isArray(parsed) ? parsed.filter((o) => typeof o === "string") : [];
} catch {
return [];
}
})();
const TRUSTED_ORIGINS = STATIC_TRUSTED_ORIGINS.concat(runtimeTrustedOrigins);
const isTrustedOrigin = (origin: string) =>
TRUSTED_ORIGINS.indexOf(origin) !== -1 ||
(origin.startsWith("https://") && origin.endsWith(".deco.cx")) ||
origin === WINDOW.location.origin;

const onMessage = (event: MessageEvent<LiveEvent>) => {
if (!isTrustedOrigin(event.origin)) {
return;
}

const { data } = event;

if (
typeof data !== "object" || data === null ||
typeof (data as { type?: unknown }).type !== "string"
) {
return;
}

switch (data.type) {
Comment thread
0xcucumbersalad marked this conversation as resolved.
case "scrollToComponent": {
const findById = document
Expand Down Expand Up @@ -183,6 +220,13 @@ function LiveControls({ site, page, flags }: Props) {
__html: JSON.stringify({ page, site, flags }),
}}
/>
<script
type="application/json"
id="__DECO_TRUSTED_ORIGINS"
dangerouslySetInnerHTML={{
__html: JSON.stringify(adminDomains),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Escape < characters in the JSON output to prevent potential script tag breakout. The HTML parser treats </script> as a closing tag regardless of type="application/json", so if adminDomains ever contains a string with </script>, it would allow injection. Apply .replace(/</g, "\\u003c") after JSON.stringify() for defense in depth.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/LiveControls.tsx, line 227:

<comment>Escape `<` characters in the JSON output to prevent potential script tag breakout. The HTML parser treats `</script>` as a closing tag regardless of `type="application/json"`, so if `adminDomains` ever contains a string with `</script>`, it would allow injection. Apply `.replace(/</g, "\\u003c")` after `JSON.stringify()` for defense in depth.</comment>

<file context>
@@ -197,6 +220,13 @@ function LiveControls({ site, page, flags }: Props) {
+        type="application/json"
+        id="__DECO_TRUSTED_ORIGINS"
+        dangerouslySetInnerHTML={{
+          __html: JSON.stringify(adminDomains),
+        }}
+      />
</file context>
Suggested change
__html: JSON.stringify(adminDomains),
__html: JSON.stringify(adminDomains).replace(/</g, "\\u003c"),

}}
/>
Comment on lines +223 to +229

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect adminDomains declaration and its source in utils/admin.ts

fd -i 'admin.ts' --type f | xargs grep -n 'adminDomains\|ADMIN_DOMAINS\|env\|Deno.env\|process.env' -A3 -B1

Repository: deco-cx/deco

Length of output: 868


🏁 Script executed:

fd -type f -name 'admin.ts' | xargs head -60 | cat -n

Repository: deco-cx/deco

Length of output: 227


🏁 Script executed:

fd -t f -name 'admin.ts' | xargs cat | head -60

Repository: deco-cx/deco

Length of output: 287


🏁 Script executed:

fd -t f -name 'admin.ts' -x cat {} | head -60

Repository: deco-cx/deco

Length of output: 287


🏁 Script executed:

git ls-files | grep -i 'admin.ts' | xargs cat

Repository: deco-cx/deco

Length of output: 1829


JSON.stringify(adminDomains) in a <script> tag should escape < characters for defense in depth.

While adminDomains is populated from the ADMIN_DOMAINS environment variable and validated as valid URL origins (which cannot naturally contain </script> sequences), embedding JSON directly in <script> tags without escaping < is an anti-pattern that should be avoided defensively. The HTML parser treats </ as a tag terminator regardless of the script type, so any future code changes or data sources that bypass the current URL validation could introduce XSS.

The fix is a one-liner and should be applied to prevent this pattern from being copied elsewhere:

🛡️ Proposed fix
- __html: JSON.stringify(adminDomains),
+ __html: JSON.stringify(adminDomains).replace(/</g, "\\u003c"),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<script
type="application/json"
id="__DECO_TRUSTED_ORIGINS"
dangerouslySetInnerHTML={{
__html: JSON.stringify(adminDomains),
}}
/>
<script
type="application/json"
id="__DECO_TRUSTED_ORIGINS"
dangerouslySetInnerHTML={{
__html: JSON.stringify(adminDomains).replace(/</g, "\\u003c"),
}}
/>
🧰 Tools
🪛 ast-grep (0.42.1)

[warning] 225-225: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/LiveControls.tsx` around lines 223 - 229, The embedded JSON in
LiveControls.tsx currently uses dangerouslySetInnerHTML with
JSON.stringify(adminDomains) (the script tag id="__DECO_TRUSTED_ORIGINS"), which
doesn't escape '<' characters; change the value to a sanitized string by calling
JSON.stringify(adminDomains).replace(/</g, '\\u003c') (or equivalent) before
assigning to __html so any '<' is escaped and the script is safe against
accidental </script> sequences while preserving the existing adminDomains data
and use of dangerouslySetInnerHTML.

<script
type="module"
dangerouslySetInnerHTML={{
Expand Down
Loading