Skip to content

Security: sandbox Web Page iframe and enforce http(s) URL allowlist (CWE-1021/CWE-829) - #11

Open
jakexcosme wants to merge 1 commit into
masterfrom
devin/1785263245-webpage-iframe-sandbox
Open

Security: sandbox Web Page iframe and enforce http(s) URL allowlist (CWE-1021/CWE-829)#11
jakexcosme wants to merge 1 commit into
masterfrom
devin/1785263245-webpage-iframe-sandbox

Conversation

@jakexcosme

@jakexcosme jakexcosme commented Jul 28, 2026

Copy link
Copy Markdown

Describe your changes:

Security fix (CWE-1021 / CWE-829): Web Page object embedded an attacker-influenced URL in an unrestricted iframe (no sandbox, no CSP frame-src).

Finding & source→sink evidence

  • Source: webPage domain-object url is a free-text textfield (src/plugins/webPage/plugin.js) persisted in shared storage (CouchDB/localStorage). It is attacker-influenceable via Import-from-JSON (src/plugins/importFromJSONAction/ImportFromJSONAction.js onSave ingests an arbitrary object graph into openmct.objects.save) or any write access to shared persistence — no per-user ownership check and no shipped authentication.
  • Sink: src/plugins/webPage/components/WebPage.vue rendered <iframe :src="url"> where url = sanitizeUrl(currentDomainObject.url). @braintree/sanitize-url only neutralizes dangerous schemes (javascript:, data:, vbscript:); it passes any external http(s)://evil.example origin unchanged, and the iframe had no sandbox attribute and no referrer policy. index.html ships no Content-Security-Policy (frame-src).
  • Impact: an adversary who controls persistence or tricks an operator into importing/opening a shared layout can make the trusted mission-ops console embed an arbitrary external page which runs its own scripts and can attempt top-frame navigation — clickjacking, credential-phishing overlays, drive-by browser exploitation, console-wide redirection.

Runtime reproduction (pre-fix)

  1. npm start, open Open MCT in a browser.
  2. Create > Web Page, set URL to https://attacker.example/phish (or import a crafted JSON export containing a webPage object with that url).
  3. Open the Web Page object: the external page renders in an unsandboxed iframe inside the console; on a user gesture, framed content can navigate window.top (e.g. <a href="..." target="_top">), redirecting the whole console to a phishing page.

Fix

src/plugins/webPage/components/WebPage.vue:

-    <iframe :src="url"></iframe>
+    <iframe :src="url" sandbox="allow-scripts allow-forms" referrerpolicy="no-referrer"></iframe>
url() {
  const url = sanitizeUrl(this.currentDomainObject.url);
  const isSafeAbsoluteUrl = /^https?:\/\//i.test(url);
  const isSafeRelativeUrl = /^\/(?!\/)/.test(url);
  if (!isSafeAbsoluteUrl && !isSafeRelativeUrl) {
    console.warn('Blocked unsafe URL:', url);
    return 'about:blank';   // BLOCKED_URL
  }
  return url;
}
  • sandbox="allow-scripts allow-forms" — deliberately omits allow-top-navigation, allow-popups, and allow-same-origin: framed content can no longer navigate the top frame, open popups, or reach the console origin's storage/DOM, breaking the clickjacking/redirect/tabnabbing path even for allowed URLs.
  • referrerpolicy="no-referrer" — prevents leaking the console URL to framed origins.
  • Scheme/origin allowlist — only http(s):// absolute URLs and same-origin root-relative paths (/..., rejecting scheme-relative //...) are bound to the iframe; anything else renders about:blank. Mirrors the existing pattern in src/plugins/imagery/actions/OpenImageInNewTabAction.js. Defense-in-depth on top of sanitizeUrl.
  • Deployment hardening (integrator-owned, out of scope for this code change): add a CSP frame-src allowlist in index.html/server headers to restrict which origins may be framed at all.

Compliance mapping

Framework Mapping
CWE CWE-1021 (Improper Restriction of Rendered UI Layers / clickjacking), CWE-829 (Inclusion of Functionality from Untrusted Control Sphere)
MITRE ATT&CK T1189 Drive-by Compromise (tactic: TA0001 Initial Access)
NIST 800-53 SC-18 (Mobile Code), SI-10 (Information Input Validation)
STIG APSC-DV-002530 (mobile code restrictions), APSC-DV-002560 / V-220631-style input validation (whitelist)

Verification (all run on this branch)

  • npm run lint — pass (eslint js/vue + cspell)
  • npm run build — pass (webpack prod + tsc)
  • npm test — 975/975 SUCCESS (Karma, ChromeHeadless)
  • Runtime smoke test: Web Page object with an https:// URL still renders; framed content can no longer navigate the top frame; non-http(s) values render about:blank.

Original mission prompt (verbatim, for AskDevin)

Fix the following security finding in `src/plugins/webPage/components/WebPage.vue:24,40` in COG-GTM/openmct:

**Web Page object embeds attacker-influenced URL in an unrestricted iframe (no sandbox / no CSP frame-src)**

Analyze the vulnerable code, implement a fix, and open a pull request with the remediation.

All Submissions:

  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't other open Pull Requests for the same update/change?
  • Is this a notable change that will require a special callout in the release notes? For example, will this break compatibility with existing APIs or projects that consume these plugins?

Author Checklist

  • Changes address original issue?
  • Tests included and/or updated with changes?
  • Has this been smoke tested?
  • Have you associated this PR with a type: label? Note: this is not necessarily the same as the original issue.
  • Have you associated a milestone with this PR? Note: leave blank if unsure.
  • Testing instructions included in associated issue OR is this a dependency/testcase change?

Reviewer Checklist

  • Changes appear to address issue?
  • Reviewer has tested changes by following the provided instructions?
  • Changes appear not to be breaking changes?
  • Appropriate automated tests included?
  • Code style and in-line documentation are appropriate?

Link to Devin session: https://app.devin.ai/sessions/8b55f4e40e1e49559e0d3ee19ab7165b
Requested by: @jakexcosme


Devin Review

Status Commit
⚪ Not started

Run Devin Review

💡 Connect your GitHub account to enable automatic code reviews.

Open in Devin Review (Staging)
Open in Devin Review

Co-Authored-By: Jake Cosme <jake@cognition.ai>
@jakexcosme jakexcosme self-assigned this Jul 28, 2026
@devin-ai-integration

Copy link
Copy Markdown

Prompt hidden (unlisted session)

@devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

Open in Devin Review

<template>
<div class="l-iframe abs">
<iframe :src="url"></iframe>
<iframe :src="url" sandbox="allow-scripts allow-forms" referrerpolicy="no-referrer"></iframe>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Sandbox omits allow-same-origin, disabling storage/DOM for same-origin relative URLs

The sandbox="allow-scripts allow-forms" attribute deliberately omits allow-same-origin. This means even the allowed same-origin root-relative URLs (/...) matched by isSafeRelativeUrl at src/plugins/webPage/components/WebPage.vue:46 will be loaded into a unique opaque origin, so framed same-origin pages will lose access to cookies, localStorage, and same-origin DOM/XHR. This is the intended security tradeoff per the PR, but reviewers should confirm no existing deployment relies on embedding a same-origin page that needs storage/session access, since such pages may now silently break.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Intended. Omitting allow-same-origin is deliberate: with it, an attacker-supplied same-origin (or allow-scripts+allow-same-origin) frame could script its way out of the sandbox, defeating the fix. The Web Page object is designed to embed external content (its url is free-text and, per the finding, attacker-influenceable via Import-from-JSON/shared persistence), so treating the frame as an untrusted opaque origin is correct. There is no known Open MCT feature that relies on embedding a same-origin page needing cookie/localStorage access through this object. Flagging the tradeoff for reviewers, but not changing it.

Comment on lines +45 to +53
const isSafeAbsoluteUrl = /^https?:\/\//i.test(url);
const isSafeRelativeUrl = /^\/(?!\/)/.test(url);
if (!isSafeAbsoluteUrl && !isSafeRelativeUrl) {
console.warn('Blocked unsafe URL:', url);

return BLOCKED_URL;
}

return url;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Behavior change: previously-rendered scheme-less and relative URLs now blocked

Previously the iframe bound whatever sanitizeUrl returned, which passes through values like 123, example.com/page (scheme-less), or scheme-relative //host/path. The new allowlist at src/plugins/webPage/components/WebPage.vue:45-51 only permits http(s):// absolute URLs and single-slash root-relative paths, rendering everything else as about:blank. Existing persisted Web Page objects whose URLs lack a scheme (a common user habit, e.g. entering www.example.com) will stop rendering and show a blank frame with only a console warning. This is intentional hardening but is a user-visible behavior change worth noting in release notes given the type description invites free-text URLs.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correct and intended. Scheme-less values like www.example.com or 123 were already broken/ambiguous as iframe sources; blocking them (rendering about:blank + a console warning) is the safe, explicit behavior and closes the scheme-relative //host bypass. This mirrors the existing allowlist in OpenImageInNewTabAction. I've noted it as a user-visible behavior change; the "notable change / release notes" checkbox is left for the maintainer to confirm. Not changing behavior here.

@devin-ai-integration

Copy link
Copy Markdown

Tested end-to-end against a local dev server (npm start, localhost:8080) — all 3 scenarios passed.

Attack path blocked (main test): Pointed a Web Page object at a local malicious page containing a target="_top" link and a script running window.top.location = 'https://example.org'. Both breakout attempts were blocked by the sandbox — the script threw a permission error (shown on-page below) and clicking the _top link did not navigate; Open MCT stayed loaded.

Malicious page blocked by sandbox

Golden path — https URL still renders (regression)

https://example.com renders normally; iframe carries sandbox="allow-scripts allow-forms" and referrerpolicy="no-referrer".

example.com renders in sandboxed iframe

URL validation — non-http URL blocked

ftp://x → iframe src="about:blank", console: Blocked unsafe URL: ftp://x.

ftp URL renders about:blank

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant