Skip to content

[remote-ui] Validate values received from the remote environment - #739

Open
greatscotty wants to merge 1 commit into
Shopify:remote-uifrom
greatscotty:validate-untrusted-remote-values-remote-ui
Open

greatscotty wants to merge 1 commit into
Shopify:remote-uifrom
greatscotty:validate-untrusted-remote-values-remote-ui

Conversation

@greatscotty

Copy link
Copy Markdown

What this changes

remote-ui counterpart to #738.

The remote environment is untrusted, but createRemoteReceiver() stored text and props without checking their shape, and @remote-ui/react’s RemoteComponent spread remote props straight onto host components. Hosts that run this package on preact/compat are exposed to Preact’s virtual-node detection (constructor === undefined), and the Vue host to Vue’s (__v_isVNode: true). Such objects survive @remote-ui/rpc’s encoder (it rebuilds objects by spreading own keys, so an own constructor: undefined key is preserved), so a remote could render arbitrary markup – including dangerouslySetInnerHTML – on the host via a text node (H1 4072911) or a prop (H1 4133346).

@remote-ui/core

  • New validation.ts, exported from the package: normalizeRemoteText, assertSafeRemoteProp/assertSafeRemoteProps, assertRemotePropName, assertRemoteComponentType.
  • createRemoteReceiver() applies them in normalizeNode (mount, insert, fragment props), updateText, and updateProps: text is normalized to a string (objects via Object.prototype.toString, never the remote’s toString), props are rejected if any object in them has constructor === undefined or __v_isVNode === true, __proto__ is rejected as a prop name, component types must be strings. Validation runs before anything is retained or attached.

@remote-ui/react

  • RemoteComponent never passes children, ref, key, dangerouslySetInnerHTML (or __proto__) from remote props to the host implementation.

Tests

  • packages/core/src/tests/validation.test.ts – helper coverage (Preact and Vue shapes, null prototypes, Map/Set/cyclic values, shapes hidden behind a redirected prototype, __proto__ names).
  • packages/core/src/tests/receiver.test.ts – receiver-level coverage for mount/insert/update/fragment-prop text and props.
  • packages/react/src/tests/untrusted-values.test.tsx – end to end through RemoteRenderer into jsdom.

Before this change the exploit was confirmed end to end with @remote-ui/core + @remote-ui/react on preact/compat: text mount/update, prop mount/update, text inside a fragment prop, and dangerouslySetInnerHTML via spread all injected attacker markup into the host DOM; all are closed after.

Full suite (loom test), loom type-check, and loom lint pass.

cc @robin-drexler

The remote environment is untrusted, but `createRemoteReceiver()` stored
text and props without checking their shape, and the React host renderer
spread remote props straight onto host components. Preact (via
preact/compat) treats any object whose `constructor` resolves to `undefined`
as a virtual node, and Vue treats any object flagged `__v_isVNode` as one;
such objects survive the RPC layer, so a remote could render arbitrary
markup on the host via text nodes or props.

- Add validation helpers to `@remote-ui/core` and apply them in the receiver
  on mount, insert, text update, and props update (including fragment
  props): text is normalized to a string, props are rejected if they contain
  a framework-node shape, `__proto__` is rejected as a prop name, and
  component types must be strings.
- `@remote-ui/react` `RemoteComponent` never passes `children`, `ref`,
  `key`, or `dangerouslySetInnerHTML` from remote props to host components.
- Add tests exercising each route with smuggled payloads.

@andrewiggins andrewiggins left a comment •

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.

Can we also address the Vue code called out in the PR description?

I think we need to make the following changes to the vue package:

  • Raise the vue peer dependency in packages/vue/package.json to >=3.5.9 <4.0.0. Before 3.5.9, Vue treats any object child as a vnode (vuejs/core#12038), so the __v_isVNode check doesn't protect older versions.
  • In packages/vue/src/host/RemoteComponent.ts, filter reserved props from remote props before h(Implementation, props), mirroring RESERVED_PROPS in the React host. Cover at least innerHTML, outerHTML, textContent, key, ref, onVnode*, and __proto__. Note to cover onVnode* we'll need a little bit more complex logic than a Set since it's a prefix of props we need to cover.

'ref',
'key',
'dangerouslySetInnerHTML',
'__proto__',

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.

Let's add innerHTML and outerHTML here for preact/compat, with corresponding tests.

This branch has not been deployed

No deployments
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.

2 participants