diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 6eca915bf16..002f3c042ff 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -640,6 +640,7 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js * should handle dangerouslySetInnerHTML * should work error event on element * should not duplicate uppercased selfclosing tags +* should warn on upper case HTML tags, not SVG nor custom tags * should warn against children for void elements * should warn against dangerouslySetInnerHTML for void elements * should emit a warning once for an unnamed custom component using shady DOM @@ -663,6 +664,8 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js * should properly escape text content and attributes values * unmounts children before unsetting DOM node info * should warn about the `onScroll` issue when unsupported (IE8) +* should throw when an invalid tag name is used server-side +* should throw when an attack vector is used server-side * should throw when an invalid tag name is used * should throw when an attack vector is used * should warn about props that are no longer supported diff --git a/src/renderers/art/ReactARTFiber.js b/src/renderers/art/ReactARTFiber.js index c4fa6d7f5ea..ffa9cc419cb 100644 --- a/src/renderers/art/ReactARTFiber.js +++ b/src/renderers/art/ReactARTFiber.js @@ -408,7 +408,7 @@ const ARTRenderer = ReactFiberReconciler({ // Noop }, - commitUpdate(instance, oldProps, newProps) { + commitUpdate(instance, type, oldProps, newProps) { instance._applyProps(instance, newProps, oldProps); }, @@ -467,7 +467,7 @@ const ARTRenderer = ReactFiberReconciler({ // Noop }, - prepareUpdate(domElement, oldProps, newProps) { + prepareUpdate(domElement, type, oldProps, newProps) { return true; }, diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index eecdb8181e7..4c38e66f6be 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -97,20 +97,16 @@ var DOMRenderer = ReactFiberReconciler({ finalizeInitialChildren( domElement : Instance, + type : string, props : Props, rootContainerInstance : Container, ) : void { - // TODO: we normalize here because DOM renderer expects tag to be lowercase. - // We can change DOM renderer to compare special case against upper case, - // and use tagName (which is upper case for HTML DOM elements). Or we could - // let the renderer "normalize" the fiber type so we don't have to read - // the type from DOM. However we need to remember SVG is case-sensitive. - var tag = domElement.tagName.toLowerCase(); - setInitialProperties(domElement, tag, props, rootContainerInstance); + setInitialProperties(domElement, type, props, rootContainerInstance); }, prepareUpdate( domElement : Instance, + type : string, oldProps : Props, newProps : Props ) : boolean { @@ -119,21 +115,16 @@ var DOMRenderer = ReactFiberReconciler({ commitUpdate( domElement : Instance, + type : string, oldProps : Props, newProps : Props, rootContainerInstance : Container, internalInstanceHandle : Object, ) : void { - // TODO: we normalize here because DOM renderer expects tag to be lowercase. - // We can change DOM renderer to compare special case against upper case, - // and use tagName (which is upper case for HTML DOM elements). Or we could - // let the renderer "normalize" the fiber type so we don't have to read - // the type from DOM. However we need to remember SVG is case-sensitive. - var tag = domElement.tagName.toLowerCase(); // Update the internal instance handle so that we know which props are // the current ones. precacheFiberNode(internalInstanceHandle, domElement); - updateProperties(domElement, tag, oldProps, newProps, rootContainerInstance); + updateProperties(domElement, type, oldProps, newProps, rootContainerInstance); }, shouldSetTextContent(props : Props) : boolean { diff --git a/src/renderers/dom/fiber/ReactDOMFiberComponent.js b/src/renderers/dom/fiber/ReactDOMFiberComponent.js index 826dcc5b6a0..44afff611e8 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberComponent.js +++ b/src/renderers/dom/fiber/ReactDOMFiberComponent.js @@ -279,21 +279,6 @@ var voidElementTags = { ...omittedCloseTags, }; -// We accept any tag to be rendered but since this gets injected into arbitrary -// HTML, we want to make sure that it's a safe tag. -// http://www.w3.org/TR/REC-xml/#NT-Name - -var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset -var validatedTagCache = {}; -var hasOwnProperty = {}.hasOwnProperty; - -function validateDangerousTag(tag) { - if (!hasOwnProperty.call(validatedTagCache, tag)) { - invariant(VALID_TAG_REGEX.test(tag), 'Invalid tag: %s', tag); - validatedTagCache[tag] = true; - } -} - function isCustomComponent(tagName, props) { return tagName.indexOf('-') >= 0 || props.is != null; } @@ -488,18 +473,23 @@ var ReactDOMFiberComponent = { rootContainerElement : Element, parentNamespace : string | null ) : Element { - validateDangerousTag(type); - // TODO: - // const tag = type.toLowerCase(); Do we need to apply lower case only on non-custom elements? - // We create tags in the namespace of their parent container, except HTML // tags get no namespace. var ownerDocument = rootContainerElement.ownerDocument; var domElement : Element; var namespaceURI = parentNamespace || getIntrinsicNamespace(type); if (namespaceURI == null) { - const tag = type.toLowerCase(); - if (tag === 'script') { + if (__DEV__) { + warning( + type === type.toLowerCase() || + isCustomComponent(type, props), + '<%s /> is using uppercase HTML. Always use lowercase HTML tags ' + + 'in React.', + type + ); + } + + if (type === 'script') { // Create the script via .innerHTML so its "parser-inserted" flag is // set to true and it does not execute var div = ownerDocument.createElement('div'); diff --git a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js index 2a263cc63d6..6c5661e6663 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js @@ -14,6 +14,7 @@ describe('ReactDOMComponent', () => { var React; + var ReactTestUtils; var ReactDOM; var ReactDOMFeatureFlags; var ReactDOMServer; @@ -29,16 +30,11 @@ describe('ReactDOMComponent', () => { ReactDOM = require('ReactDOM'); ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); ReactDOMServer = require('ReactDOMServer'); + ReactTestUtils = require('ReactTestUtils'); inputValueTracking = require('inputValueTracking'); }); describe('updateDOM', () => { - var ReactTestUtils; - - beforeEach(() => { - ReactTestUtils = require('ReactTestUtils'); - }); - it('should handle className', () => { var container = document.createElement('div'); ReactDOM.render(
, container); @@ -795,6 +791,7 @@ describe('ReactDOMComponent', () => { }); it('should not duplicate uppercased selfclosing tags', () => { + spyOn(console, 'error'); class Container extends React.Component { render() { return React.createElement('BR', null); @@ -803,6 +800,27 @@ describe('ReactDOMComponent', () => { var returnedValue = ReactDOMServer.renderToString(); expect(returnedValue).not.toContain('
'); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + '
is using uppercase HTML.' + ); + }); + + it('should warn on upper case HTML tags, not SVG nor custom tags', () => { + spyOn(console, 'error'); + ReactTestUtils.renderIntoDocument( + React.createElement('svg', null, React.createElement('PATH')) + ); + expectDev(console.error.calls.count()).toBe(0); + ReactTestUtils.renderIntoDocument( + React.createElement('CUSTOM-TAG') + ); + expectDev(console.error.calls.count()).toBe(0); + ReactTestUtils.renderIntoDocument(React.createElement('IMG')); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + ' is using uppercase HTML.' + ); }); it('should warn against children for void elements', () => { @@ -1171,8 +1189,7 @@ describe('ReactDOMComponent', () => { .mock('isEventSupported'); var isEventSupported = require('isEventSupported'); isEventSupported.mockReturnValueOnce(false); - - var ReactTestUtils = require('ReactTestUtils'); + ReactTestUtils = require('ReactTestUtils'); spyOn(console, 'error'); ReactTestUtils.renderIntoDocument(
); @@ -1190,34 +1207,40 @@ describe('ReactDOMComponent', () => { }); describe('tag sanitization', () => { - it('should throw when an invalid tag name is used', () => { - var ReactTestUtils = require('ReactTestUtils'); + it('should throw when an invalid tag name is used server-side', () => { var hackzor = React.createElement('script tag'); expect( - () => ReactTestUtils.renderIntoDocument(hackzor) + () => ReactDOMServer.renderToString(hackzor) ).toThrowError( 'Invalid tag: script tag' ); }); - it('should throw when an attack vector is used', () => { - var ReactTestUtils = require('ReactTestUtils'); + it('should throw when an attack vector is used server-side', () => { var hackzor = React.createElement('div> ReactTestUtils.renderIntoDocument(hackzor) + () => ReactDOMServer.renderToString(hackzor) ).toThrowError( 'Invalid tag: div> { - var ReactTestUtils; + it('should throw when an invalid tag name is used', () => { + var hackzor = React.createElement('script tag'); + expect( + () => ReactTestUtils.renderIntoDocument(hackzor) + ).toThrow(); + }); - beforeEach(() => { - ReactTestUtils = require('ReactTestUtils'); + it('should throw when an attack vector is used', () => { + var hackzor = React.createElement('div> ReactTestUtils.renderIntoDocument(hackzor) + ).toThrow(); }); + }); + describe('nesting validation', () => { it('warns on invalid nesting', () => { spyOn(console, 'error'); ReactTestUtils.renderIntoDocument(
); diff --git a/src/renderers/dom/stack/client/ReactDOMComponent.js b/src/renderers/dom/stack/client/ReactDOMComponent.js index 13f61ae8c4c..b23a736b083 100644 --- a/src/renderers/dom/stack/client/ReactDOMComponent.js +++ b/src/renderers/dom/stack/client/ReactDOMComponent.js @@ -413,7 +413,6 @@ var globalIdCounter = 1; */ function ReactDOMComponent(element) { var tag = element.type; - validateDangerousTag(tag); this._currentElement = element; this._tag = tag.toLowerCase(); this._namespaceURI = null; @@ -524,6 +523,15 @@ ReactDOMComponent.Mixin = { namespaceURI = DOMNamespaces.html; } if (namespaceURI === DOMNamespaces.html) { + if (__DEV__) { + warning( + isCustomComponent(this._tag, props) || + this._tag === this._currentElement.type, + '<%s /> is using uppercase HTML. Always use lowercase HTML tags ' + + 'in React.', + this._currentElement.type + ); + } if (this._tag === 'svg') { namespaceURI = DOMNamespaces.svg; } else if (this._tag === 'math') { @@ -596,6 +604,7 @@ ReactDOMComponent.Mixin = { this._createInitialChildren(transaction, props, context, lazyTree); mountImage = lazyTree; } else { + validateDangerousTag(this._tag); var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props); var tagContent = this._createContentMarkup(transaction, props, context); if (!tagContent && omittedCloseTags[this._tag]) { diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index 97cb1578faa..d16b6e7bf2f 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -60,15 +60,15 @@ var NoopRenderer = ReactFiberReconciler({ parentInstance.children.push(child); }, - finalizeInitialChildren(domElement : Instance, props : Props) : void { + finalizeInitialChildren(domElement : Instance, type : string, props : Props) : void { // Noop }, - prepareUpdate(instance : Instance, oldProps : Props, newProps : Props) : boolean { + prepareUpdate(instance : Instance, type : string, oldProps : Props, newProps : Props) : boolean { return true; }, - commitUpdate(instance : Instance, oldProps : Props, newProps : Props) : void { + commitUpdate(instance : Instance, type : string, oldProps : Props, newProps : Props) : void { instance.prop = newProps.prop; }, diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 46f0fdc94b5..5ff31e2d514 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -373,7 +373,8 @@ module.exports = function( const newProps = finishedWork.memoizedProps; const oldProps = current.memoizedProps; const rootContainerInstance = getRootHostContainer(); - commitUpdate(instance, oldProps, newProps, rootContainerInstance, finishedWork); + const type = finishedWork.type; + commitUpdate(instance, type, oldProps, newProps, rootContainerInstance, finishedWork); } detachRefIfNeeded(current, finishedWork); return; diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 84af8a456d8..ea85f1fe60a 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -215,6 +215,7 @@ module.exports = function( } case HostComponent: popHostContext(workInProgress); + const type = workInProgress.type; let newProps = workInProgress.pendingProps; if (current && workInProgress.stateNode != null) { // If we have an alternate, that means this is an update and we need to @@ -228,7 +229,7 @@ module.exports = function( newProps = workInProgress.memoizedProps || oldProps; } const instance : I = workInProgress.stateNode; - if (prepareUpdate(instance, oldProps, newProps)) { + if (prepareUpdate(instance, type, oldProps, newProps)) { // This returns true if there was something to update. markUpdate(workInProgress); } @@ -249,14 +250,14 @@ module.exports = function( // or completeWork depending on we want to add then top->down or // bottom->up. Top->down is faster in IE11. const instance = createInstance( - workInProgress.type, + type, newProps, rootContainerInstance, currentHostContext, workInProgress ); appendAllChildren(instance, workInProgress); - finalizeInitialChildren(instance, newProps, rootContainerInstance); + finalizeInitialChildren(instance, type, newProps, rootContainerInstance); workInProgress.stateNode = instance; if (workInProgress.ref) { diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 08204a048be..571d9aa921d 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -46,10 +46,10 @@ export type HostConfig = { createInstance(type : T, props : P, rootContainerInstance : C, hostContext : CX | null, internalInstanceHandle : OpaqueNode) : I, appendInitialChild(parentInstance : I, child : I | TI) : void, - finalizeInitialChildren(parentInstance : I, props : P, rootContainerInstance : C) : void, + finalizeInitialChildren(parentInstance : I, type : T, props : P, rootContainerInstance : C) : void, - prepareUpdate(instance : I, oldProps : P, newProps : P) : boolean, - commitUpdate(instance : I, oldProps : P, newProps : P, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : void, + prepareUpdate(instance : I, type : T, oldProps : P, newProps : P) : boolean, + commitUpdate(instance : I, type : T, oldProps : P, newProps : P, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : void, shouldSetTextContent(props : P) : boolean, resetTextContent(instance : I) : void,