Security: sandbox Web Page iframe and enforce http(s) URL allowlist (CWE-1021/CWE-829) - #11
Security: sandbox Web Page iframe and enforce http(s) URL allowlist (CWE-1021/CWE-829)#11jakexcosme wants to merge 1 commit into
Conversation
Co-Authored-By: Jake Cosme <jake@cognition.ai>
|
Prompt hidden (unlisted session) |
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| <template> | ||
| <div class="l-iframe abs"> | ||
| <iframe :src="url"></iframe> | ||
| <iframe :src="url" sandbox="allow-scripts allow-forms" referrerpolicy="no-referrer"></iframe> |
There was a problem hiding this comment.
🔍 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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.
| const isSafeAbsoluteUrl = /^https?:\/\//i.test(url); | ||
| const isSafeRelativeUrl = /^\/(?!\/)/.test(url); | ||
| if (!isSafeAbsoluteUrl && !isSafeRelativeUrl) { | ||
| console.warn('Blocked unsafe URL:', url); | ||
|
|
||
| return BLOCKED_URL; | ||
| } | ||
|
|
||
| return url; |
There was a problem hiding this comment.
📝 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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.
|
Tested end-to-end against a local dev server ( Attack path blocked (main test): Pointed a Web Page object at a local malicious page containing a Golden path — https URL still renders (regression)
|
Describe your changes:
Security fix (CWE-1021 / CWE-829): Web Page object embedded an attacker-influenced URL in an unrestricted iframe (no
sandbox, no CSPframe-src).Finding & source→sink evidence
webPagedomain-objecturlis a free-texttextfield(src/plugins/webPage/plugin.js) persisted in shared storage (CouchDB/localStorage). It is attacker-influenceable via Import-from-JSON (src/plugins/importFromJSONAction/ImportFromJSONAction.jsonSaveingests an arbitrary object graph intoopenmct.objects.save) or any write access to shared persistence — no per-user ownership check and no shipped authentication.src/plugins/webPage/components/WebPage.vuerendered<iframe :src="url">whereurl = sanitizeUrl(currentDomainObject.url).@braintree/sanitize-urlonly neutralizes dangerous schemes (javascript:,data:,vbscript:); it passes any externalhttp(s)://evil.exampleorigin unchanged, and the iframe had nosandboxattribute and no referrer policy.index.htmlships no Content-Security-Policy (frame-src).Runtime reproduction (pre-fix)
npm start, open Open MCT in a browser.https://attacker.example/phish(or import a crafted JSON export containing awebPageobject with thaturl).window.top(e.g.<a href="..." target="_top">), redirecting the whole console to a phishing page.Fix
src/plugins/webPage/components/WebPage.vue:sandbox="allow-scripts allow-forms"— deliberately omitsallow-top-navigation,allow-popups, andallow-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.http(s)://absolute URLs and same-origin root-relative paths (/..., rejecting scheme-relative//...) are bound to the iframe; anything else rendersabout:blank. Mirrors the existing pattern insrc/plugins/imagery/actions/OpenImageInNewTabAction.js. Defense-in-depth on top ofsanitizeUrl.frame-srcallowlist inindex.html/server headers to restrict which origins may be framed at all.Compliance mapping
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)https://URL still renders; framed content can no longer navigate the top frame; non-http(s) values renderabout:blank.Original mission prompt (verbatim, for AskDevin)
All Submissions:
Author Checklist
type:label? Note: this is not necessarily the same as the original issue.Reviewer Checklist
Link to Devin session: https://app.devin.ai/sessions/8b55f4e40e1e49559e0d3ee19ab7165b
Requested by: @jakexcosme
Devin Review