fix(core): reject dynamic script host elements

The previous fix for GHSA-692r-grfm-v8x7 was incomplete because it rejected script tags only when locating an explicit host element. Dynamic component instantiation can also infer the host element from the component selector.

Move the script-host rejection to the point where ComponentFactory has resolved the host element for either path, so createComponent rejects script hosts consistently.
This commit is contained in:
SkyZeroZx
2026-06-28 00:15:56 -05:00
committed by Alex Rickabaugh
parent a68e610641
commit d7f70616a0
11 changed files with 31 additions and 7 deletions
@@ -183,6 +183,15 @@ function createHostElement(componentDef: ComponentDef<unknown>, renderer: Render
return createElementNode(renderer, tagName, namespace);
}
function assertNotScriptHostElement(tagName: string | null | undefined): void {
if (tagName?.toLowerCase() === 'script') {
throw new RuntimeError(
RuntimeErrorCode.UNSAFE_VALUE_IN_SCRIPT,
ngDevMode && `"<script>" tag is not allowed as a component host element.`,
);
}
}
/**
* Infers the tag name that should be used for a component based on its definition.
* @param componentDef Definition for which to resolve the tag name.
@@ -303,6 +312,7 @@ export class ComponentFactory<T> {
const hostElement = rootSelectorOrNode
? locateHostElement(hostRenderer, rootSelectorOrNode, cmpDef.encapsulation, rootViewInjector)
: createHostElement(cmpDef, hostRenderer);
assertNotScriptHostElement(hostElement?.tagName);
const sharedStylesHost = rootViewInjector.get(SHARED_STYLES_HOST, null);
const styleHost = getStyleHost(
@@ -15,7 +15,6 @@ import {ViewEncapsulation} from '../../metadata/view';
import {validateAgainstEventProperties} from '../../sanitization/sanitization';
import {ProfilerEvent} from '../../../primitives/devtools';
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../ng_reflect';
import {assertIndexInRange, assertNotSame} from '../../util/assert';
import {escapeCommentText} from '../../util/dom';
@@ -181,12 +180,6 @@ export function locateHostElement(
encapsulation === ViewEncapsulation.ShadowDom ||
encapsulation === ViewEncapsulation.ExperimentalIsolatedShadowDom;
const rootElement = renderer.selectRootElement(elementOrSelector, preserveContent);
if (rootElement.tagName.toLowerCase() === 'script') {
throw new RuntimeError(
RuntimeErrorCode.UNSAFE_VALUE_IN_SCRIPT,
ngDevMode && `"<script>" tag is not allowed as a component host element.`,
);
}
applyRootElementTransform(rootElement as HTMLElement);
return rootElement;
}
@@ -873,6 +873,19 @@ describe('innerHTML processing', () => {
});
describe('Component host element validation', () => {
it('should throw an error when dynamically creating a component with a script selector', () => {
@Component({
selector: 'script',
template: '',
})
class ScriptHost {}
const environmentInjector = TestBed.inject(EnvironmentInjector);
expect(() => {
createComponent(ScriptHost, {environmentInjector});
}).toThrowError(/"<script>" tag is not allowed as a component host element/);
});
it('should throw an error when dynamically mounting a component onto a script tag', () => {
@Component({
selector: 'my-sink',
@@ -338,6 +338,7 @@
"areAnimationSupported",
"arrRemove",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertTypeDefined",
"attachPatchData",
"balancePreviousStylesIntoKeyframes",
@@ -263,6 +263,7 @@
"areAnimationSupported",
"arrRemove",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertTypeDefined",
"attachPatchData",
"baseElement",
@@ -309,6 +309,7 @@
"areAnimationSupported",
"arrRemove",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertTypeDefined",
"attachPatchData",
"bind",
@@ -389,6 +389,7 @@
"assertAllValuesPresent",
"assertControlPresent",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertPlatform",
"assertTypeDefined",
"attachPatchData",
@@ -390,6 +390,7 @@
"assertAllValuesPresent",
"assertControlPresent",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertPlatform",
"assertTypeDefined",
"attachPatchData",
@@ -375,6 +375,7 @@
"areAnimationSupported",
"arrRemove",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertTypeDefined",
"attachPatchData",
"baseElement",
@@ -423,6 +423,7 @@
"arrRemove",
"arrayEquals",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertTypeDefined",
"attachPatchData",
"baseElement",
@@ -244,6 +244,7 @@
"areAnimationSupported",
"arrRemove",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertTypeDefined",
"attachPatchData",
"baseElement",