fix(ivy): throw on bindings to unknown properties (#28537)
This commit adds a devMode-only check which will throw if a user attempts to bind a property that does not match a directive input or a known HTML property. Example: ``` <div [unknownProp]="someValue"></div> ``` The above will throw because "unknownProp" is not a known property of HTMLDivElement. This check is similar to the check executed in View Engine during template parsing, but occurs at runtime instead of compile-time. Note: This change uncovered an existing bug with host binding inheritance, so some Material tests had to be turned off. They will be fixed in an upcoming PR. PR Close #28537
This commit is contained in:

committed by
Miško Hevery

parent
7660d0d74a
commit
1950e2d9ba
@ -10,7 +10,7 @@ import {InjectFlags, InjectionToken, Injector} from '../di';
|
||||
import {resolveForwardRef} from '../di/forward_ref';
|
||||
import {ErrorHandler} from '../error_handler';
|
||||
import {Type} from '../interface/type';
|
||||
import {validateAttribute, validateProperty} from '../sanitization/sanitization';
|
||||
import {validateAgainstEventAttributes, validateAgainstEventProperties} from '../sanitization/sanitization';
|
||||
import {Sanitizer} from '../sanitization/security';
|
||||
import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
|
||||
import {assertDataInRange, assertDefined, assertEqual, assertLessThan, assertNotEqual} from '../util/assert';
|
||||
@ -39,7 +39,7 @@ import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector
|
||||
import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode} from './state';
|
||||
import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
|
||||
import {BoundPlayerFactory} from './styling/player_factory';
|
||||
import {createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util';
|
||||
import {ANIMATION_PROP_PREFIX, createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util';
|
||||
import {NO_CHANGE} from './tokens';
|
||||
import {INTERPOLATION_DELIMITER, findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util';
|
||||
|
||||
@ -1099,7 +1099,7 @@ export function elementAttribute(
|
||||
index: number, name: string, value: any, sanitizer?: SanitizerFn | null,
|
||||
namespace?: string): void {
|
||||
if (value !== NO_CHANGE) {
|
||||
ngDevMode && validateAttribute(name);
|
||||
ngDevMode && validateAgainstEventAttributes(name);
|
||||
const lView = getLView();
|
||||
const renderer = lView[RENDERER];
|
||||
const element = getNativeByIndex(index, lView);
|
||||
@ -1193,7 +1193,8 @@ function elementPropertyInternal<T>(
|
||||
}
|
||||
} else if (tNode.type === TNodeType.Element) {
|
||||
if (ngDevMode) {
|
||||
validateProperty(propName);
|
||||
validateAgainstEventProperties(propName);
|
||||
validateAgainstUnknownProperties(element, propName, tNode);
|
||||
ngDevMode.rendererSetProperty++;
|
||||
}
|
||||
|
||||
@ -1212,6 +1213,18 @@ function elementPropertyInternal<T>(
|
||||
}
|
||||
}
|
||||
|
||||
function validateAgainstUnknownProperties(
|
||||
element: RElement | RComment, propName: string, tNode: TNode) {
|
||||
// If prop is not a known property of the HTML element...
|
||||
if (!(propName in element) &&
|
||||
// and isn't a synthetic animation property...
|
||||
propName[0] !== ANIMATION_PROP_PREFIX) {
|
||||
// ... it is probably a user error and we should throw.
|
||||
throw new Error(
|
||||
`Template error: Can't bind to '${propName}' since it isn't a known property of '${tNode.tagName}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores debugging data for this property binding on first template pass.
|
||||
* This enables features like DebugElement.properties.
|
||||
|
@ -20,7 +20,7 @@ import {getTNode} from '../util';
|
||||
|
||||
import {CorePlayerHandler} from './core_player_handler';
|
||||
|
||||
const ANIMATION_PROP_PREFIX = '@';
|
||||
export const ANIMATION_PROP_PREFIX = '@';
|
||||
|
||||
export function createEmptyStylingContext(
|
||||
element?: RElement | null, sanitizer?: StyleSanitizeFn | null,
|
||||
|
@ -178,7 +178,7 @@ export const defaultStyleSanitizer = (function(prop: string, value?: string): st
|
||||
return sanitizeStyle(value);
|
||||
} as StyleSanitizeFn);
|
||||
|
||||
export function validateProperty(name: string) {
|
||||
export function validateAgainstEventProperties(name: string) {
|
||||
if (name.toLowerCase().startsWith('on')) {
|
||||
const msg = `Binding to event property '${name}' is disallowed for security reasons, ` +
|
||||
`please use (${name.slice(2)})=...` +
|
||||
@ -188,7 +188,7 @@ export function validateProperty(name: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function validateAttribute(name: string) {
|
||||
export function validateAgainstEventAttributes(name: string) {
|
||||
if (name.toLowerCase().startsWith('on')) {
|
||||
const msg = `Binding to event attribute '${name}' is disallowed for security reasons, ` +
|
||||
`please use (${name.slice(2)})=...`;
|
||||
|
Reference in New Issue
Block a user