I'm building a vanilla JavaScript library that uses Preact for its stage management and accepts JSX elements for customizing the UI.
JSX elements coming from Preact work as expected. However, React elements are ignored.
Preact considers a valid JSX element to have a constructor key set to undefined:
I'm not sure whether this is intentional to have this check now, it might have been here for legacy reasons (prior to Preact X).
For React elements to be compatible with the library I'm building, I need to loop over the children and to add the constructor key set to undefined.
{
+ constructor: undefined,
props: {
children: "Hello"
}
}
Why is constructor necessary for a Preact element to be valid?
Here is a reproduction of the issue, with a user-land fix.
This is a new safety guard that was introduced with Preact X. It guards against a possible XSS injection vulnerability, where a bad actor might pass custom vnodes and thus would be able to take control of props.dangerouslySetInnerHTML.
const BAD = {
type: "script",
props: {
dangerouslySetInnerHTML: "alert('XSS')"
}
};
render(<div>{BAD}</div>, container);
The fix ensures that no JSON structure can be passed as children.
EDIT: Note that compact works with react elements out of the box. It uses isValidElement under the hood.
As Marvin already mentioned if you alias react to preact/compat you should not have a conflict between react and preact vnodes
Do you have a usecase where you actually want to use vnodes created by react?
I see, thanks for the explanation.
In my case however, I would like users to be able to use React elements too – it should accept JSX elements coming from both Preact and React (not preact/compat). Those elements are given to a Template component that renders them as strings (for beginner users) or as (P)react elements (for advanced users and to also support Vue in the long run).
That's a bit annoying that this specific fix breaks the compatibility with React elements. Would there be another solution that doesn't rely on this constructor property and therefore makes the bridge between Preact and React easier?
It's one of the most reliable ways to prevent XSS since passing constructor: undefined in JSON doesn't result in anything but it does in JS. I do think that the concept of iterating on the children is exposing your application to the XSS risk.
Accepting elements that are not passed through our own h/createElement is tricky, because the $$typeoff property in react elements point to an internal symbol. In all the scenarios I can come up with you'd open up the gates for XSS attacks :/
@JoviDeCroock Is there some documentation somewhere that could help me understand how iterating on the children is exposing the lib to the XSS risk?
@marvinhagemeister I'm not sure if we can leverage this but React falls back to the number 0xeac7 if Symbols are not available. In terms of long-term vision, does Preact want to expose the same API as React or to support React components too?
Regarding the security implications, React has a very detailed overview of why vnode injection was an exploitable vulnerability worth mitigating against.
Preact does not implement internal APIs like vnode.$$typeof, since those are implementation details of React. Implementing such things would produce a straight clone of React, which is not what Preact aims to be.
Preact also specifically does not attempt to interoperate with React-instantiated VNodes, since any application relying on this behavior would be a highly suboptimal configuration. Creating vnodes using React means you're already running a copy of React - there isn't a reason to bring in a second library to do rendering given you've already got the first one loaded.
Regarding the original issue, you can patch this to work as follows:
import * as React from 'react'; // or however you would normally
let old = React.createElement;
React.createElement = function() {
let vnode = old.apply(this, arguments);
vnode.constructor = undefined;
return vnode;
}
I wouldn't recommend doing this (or suggesting others do so) for anything important.
Thank you for the clarifications, this is highly appreciated.
I might turn the problem around to start from a new angle then: leveraging Preact _only_ as state management and creating specialized renderers for different frameworks. I'm still trying to figure out if that's a legit use of Preact. At this point what I'm doing is experimental and I'm trying multiple approaches.
I joined the Preact Slack workspace which might be more appropriate for future related questions.
Since the original issue is answered, feel free to close it. Thank you again.
Most helpful comment
This is a new safety guard that was introduced with Preact X. It guards against a possible XSS injection vulnerability, where a bad actor might pass custom
vnodesand thus would be able to take control ofprops.dangerouslySetInnerHTML.The fix ensures that no JSON structure can be passed as
children.EDIT: Note that
compactworks with react elements out of the box. It usesisValidElementunder the hood.