|
| 1 | +import * as React from 'react'; |
| 2 | +import { |
| 3 | + BaseComponentProps, |
| 4 | + ElementType, |
| 5 | + ForwardRefPrimitive, |
| 6 | + Primitive, |
| 7 | + PrimitiveProps, |
| 8 | +} from '../../primitives/types'; |
| 9 | +import { primitiveWithForwardRef } from '../../primitives/utils/primitiveWithForwardRef'; |
| 10 | + |
| 11 | +interface BaseStyleProps extends BaseComponentProps { |
| 12 | + cssText?: string; |
| 13 | +} |
| 14 | + |
| 15 | +export type StyleProps<Element extends ElementType = 'style'> = PrimitiveProps< |
| 16 | + BaseStyleProps, |
| 17 | + Element |
| 18 | +>; |
| 19 | + |
| 20 | +const StylePrimitive: Primitive<StyleProps, 'style'> = ( |
| 21 | + { cssText, ...rest }, |
| 22 | + ref |
| 23 | +) => { |
| 24 | + /* |
| 25 | + Only inject theme CSS variables if given a theme. |
| 26 | + The CSS file users import already has the default theme variables in it. |
| 27 | + This will allow users to use the provider and theme with CSS variables |
| 28 | + without having to worry about specificity issues because this stylesheet |
| 29 | + will likely come after a user's defined CSS. |
| 30 | +
|
| 31 | + Q: Why are we using dangerouslySetInnerHTML? |
| 32 | + A: We need to directly inject the theme's CSS string into the <style> tag without typical HTML escaping. |
| 33 | + For example, JSX would escape characters meaningful in CSS such as ', ", < and >, thus breaking the CSS. |
| 34 | + Q: Why not use a sanitization library such as DOMPurify? |
| 35 | + A: For our use case, we specifically want to purify CSS text, *not* HTML. |
| 36 | + DOMPurify, as well as any other HTML sanitization library, would escape/encode meaningful CSS characters |
| 37 | + and break our CSS in the same way that JSX would. |
| 38 | +
|
| 39 | + Q: Are there any security risks in this particular use case? |
| 40 | + A: Anything set inside of a <style> tag is always interpreted as CSS text, *not* HTML. |
| 41 | + Reference: “Restrictions on the content of raw text elements” https://html.spec.whatwg.org/dev/syntax.html#cdata-rcdata-restrictions |
| 42 | + And in our case, we are using dangerouslySetInnerHTML to set CSS text inside of a <style> tag. |
| 43 | + |
| 44 | + Thus, it really comes down to the question: Could a malicious user escape from the context of the <style> tag? |
| 45 | + For example, when inserting HTML into the DOM, could someone prematurely close the </style> tag and add a <script> tag? |
| 46 | + e.g., </style><script>alert('hello')</script> |
| 47 | + The answer depends on whether the code is rendered on the client or server side. |
| 48 | +
|
| 49 | + Client side |
| 50 | + - To prevent XSS inside of the <style> tag, we need to make sure it's not closed prematurely. |
| 51 | + - This is prevented by React because React creates a style DOM node (e.g., React.createElement(‘style’, ...)), and directly sets innerHTML as a string. |
| 52 | + - Even if the string contains a closing </style> tag, it will still be interpreted as CSS text by the browser. |
| 53 | + - Therefore, there is not an XSS vulnerability on the client side. |
| 54 | +
|
| 55 | + Server side |
| 56 | + - When React code is rendered on the server side (e.g., NextJS), the code is sent to the browser as HTML text. |
| 57 | + - Therefore, it *IS* possible to insert a closing </style> tag and escape the CSS context, which opens an XSS vulnerability. |
| 58 | +
|
| 59 | + Q: How are we mitigating the potential attack vector? |
| 60 | + A: To fix this potential attack vector on the server side, we need to filter out any closing </style> tags, |
| 61 | + as this the only way to escape from the context of the browser interpreting the text as CSS. |
| 62 | + We also need to catch cases where there is any kind of whitespace character </style[HERE]>, such as tabs, carriage returns, etc: |
| 63 | + </style |
| 64 | + |
| 65 | + > |
| 66 | + Therefore, by only rendering CSS text which does not include a closing '</style>' tag, |
| 67 | + we ensure that the browser will correctly interpret all the text as CSS. |
| 68 | + */ |
| 69 | + if (cssText === undefined || /<\/style/i.test(cssText)) { |
| 70 | + return null; |
| 71 | + } |
| 72 | + |
| 73 | + return ( |
| 74 | + <style |
| 75 | + {...rest} |
| 76 | + ref={ref} |
| 77 | + // eslint-disable-next-line react/no-danger |
| 78 | + dangerouslySetInnerHTML={{ __html: cssText }} |
| 79 | + /> |
| 80 | + ); |
| 81 | +}; |
| 82 | + |
| 83 | +/** |
| 84 | + * @experimental |
| 85 | + * [📖 Docs](https://ui.docs.amplify.aws/react/components/theme) |
| 86 | + */ |
| 87 | +export const Style: ForwardRefPrimitive<BaseStyleProps, 'style'> = |
| 88 | + primitiveWithForwardRef(StylePrimitive); |
| 89 | + |
| 90 | +Style.displayName = 'Style'; |
0 commit comments