diff --git a/goldens/public-api/core/index.api.md b/goldens/public-api/core/index.api.md index 1cc47bdb4250..14d77474d587 100644 --- a/goldens/public-api/core/index.api.md +++ b/goldens/public-api/core/index.api.md @@ -1769,6 +1769,8 @@ export interface SchemaMetadata { // @public export enum SecurityContext { + // (undocumented) + ATTRIBUTE_NO_BINDING = 6, // (undocumented) HTML = 1, // (undocumented) diff --git a/integration/cli-hello-world-ivy-i18n/size.json b/integration/cli-hello-world-ivy-i18n/size.json index 7edb4e7db0fc..6e17d691f055 100644 --- a/integration/cli-hello-world-ivy-i18n/size.json +++ b/integration/cli-hello-world-ivy-i18n/size.json @@ -1,4 +1,4 @@ { - "dist/main.js": 135813, + "dist/main.js": 144843, "dist/polyfills.js": 35883 } diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index b3a7d8c7b4b4..60a4ebf3abee 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -32,7 +32,6 @@ import {publishFacade} from './jit_compiler_facade'; import * as outputAst from './output/output_ast'; import {global} from './util'; -export {SECURITY_SCHEMA} from './schema/dom_security_schema'; export {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata} from './core'; export {core}; diff --git a/packages/compiler/src/core.ts b/packages/compiler/src/core.ts index a085fa566a54..7f9f0ad0d215 100644 --- a/packages/compiler/src/core.ts +++ b/packages/compiler/src/core.ts @@ -79,16 +79,6 @@ export interface Type extends Function { } export const Type = Function; -export enum SecurityContext { - NONE = 0, - HTML = 1, - STYLE = 2, - SCRIPT = 3, - URL = 4, - RESOURCE_URL = 5, - ATTRIBUTE_NO_BINDING = 6, -} - /** * Injection flags for DI. */ @@ -331,3 +321,5 @@ export const enum AttributeMarker { */ I18n = 6, } + +export {SecurityContext} from './schema/dom_security_schema'; diff --git a/packages/compiler/src/schema/dom_element_schema_registry.ts b/packages/compiler/src/schema/dom_element_schema_registry.ts index 6da1bf761d32..8644877645e9 100644 --- a/packages/compiler/src/schema/dom_element_schema_registry.ts +++ b/packages/compiler/src/schema/dom_element_schema_registry.ts @@ -444,8 +444,6 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry { propName = this.getMappedPropName(propName); } - // Make sure comparisons are case insensitive, so that case differences between attribute and - // property names do not have a security impact. tagName = tagName.toLowerCase(); propName = propName.toLowerCase(); diff --git a/packages/compiler/src/schema/dom_security_schema.ts b/packages/compiler/src/schema/dom_security_schema.ts index f9707eff1aee..179752e79f79 100644 --- a/packages/compiler/src/schema/dom_security_schema.ts +++ b/packages/compiler/src/schema/dom_security_schema.ts @@ -6,7 +6,24 @@ * found in the LICENSE file at https://angular.dev/license */ -import {SecurityContext} from '../core'; +/** + * A SecurityContext marks a location that has dangerous security implications, e.g. a DOM property + * like `innerHTML` that could cause Cross Site Scripting (XSS) security bugs when improperly + * handled. + * + * See DomSanitizer for more details on security in Angular applications. + * + * @publicApi + */ +export enum SecurityContext { + NONE = 0, + HTML = 1, + STYLE = 2, + SCRIPT = 3, + URL = 4, + RESOURCE_URL = 5, + ATTRIBUTE_NO_BINDING = 6, +} // ================================================================================================= // ================================================================================================= @@ -18,11 +35,17 @@ import {SecurityContext} from '../core'; // // ================================================================================================= -/** Map from tagName|propertyName to SecurityContext. Properties applying to all tags use '*'. */ +/** + * Map from tagName|propertyName to SecurityContext. Properties applying to all tags use '*'. + */ let _SECURITY_SCHEMA!: {[k: string]: SecurityContext}; const SVG_NAMESPACE = 'svg'; const MATH_ML_NAMESPACE = 'math'; +/** + * @remarks Keep is a copy of DOM Security Schema. + * @see [SECURITY_SCHEMA](../../../compiler/src/schema/dom_security_schema.ts) + */ export function SECURITY_SCHEMA(): {[k: string]: SecurityContext} { if (!_SECURITY_SCHEMA) { _SECURITY_SCHEMA = {}; diff --git a/packages/compiler/src/template/pipeline/src/emit.ts b/packages/compiler/src/template/pipeline/src/emit.ts index 00529edc9943..91fc45431029 100644 --- a/packages/compiler/src/template/pipeline/src/emit.ts +++ b/packages/compiler/src/template/pipeline/src/emit.ts @@ -75,6 +75,7 @@ import {resolveDeferDepsFns} from './phases/resolve_defer_deps_fns'; import {resolveDollarEvent} from './phases/resolve_dollar_event'; import {resolveI18nElementPlaceholders} from './phases/resolve_i18n_element_placeholders'; import {resolveI18nExpressionPlaceholders} from './phases/resolve_i18n_expression_placeholders'; +import {resolveI18nAttrSanitizers} from './phases/resolve_i18n_attr_sanitizers'; import {resolveNames} from './phases/resolve_names'; import {resolveSanitizers} from './phases/resolve_sanitizers'; import {removeSafeNavigationMigration} from './phases/safe_navigation_migration'; @@ -161,6 +162,7 @@ const phases: Phase[] = [ {kind: Kind.Tmpl, fn: resolveI18nExpressionPlaceholders}, {kind: Kind.Tmpl, fn: extractI18nMessages}, {kind: Kind.Tmpl, fn: collectI18nConsts}, + {kind: Kind.Tmpl, fn: resolveI18nAttrSanitizers}, {kind: Kind.Tmpl, fn: collectConstExpressions}, {kind: Kind.Both, fn: collectElementConsts}, {kind: Kind.Tmpl, fn: removeI18nContexts}, diff --git a/packages/compiler/src/template/pipeline/src/phases/const_collection.ts b/packages/compiler/src/template/pipeline/src/phases/const_collection.ts index 6451bbdfc0f1..1a5f2201a2b9 100644 --- a/packages/compiler/src/template/pipeline/src/phases/const_collection.ts +++ b/packages/compiler/src/template/pipeline/src/phases/const_collection.ts @@ -186,10 +186,7 @@ class ElementAttributes { if (value === null) { throw Error('Attribute, i18n attribute, & style element attributes must have a value'); } - if (trustedValueFn !== null) { - if (!ir.isStringLiteral(value)) { - throw Error('AssertionError: extracted attribute value should be string literal'); - } + if (trustedValueFn !== null && ir.isStringLiteral(value)) { array.push( o.taggedTemplate( trustedValueFn, diff --git a/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_attr_sanitizers.ts b/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_attr_sanitizers.ts new file mode 100644 index 000000000000..6b4252be9d60 --- /dev/null +++ b/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_attr_sanitizers.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {SecurityContext} from '../../../../core'; +import * as o from '../../../../output/output_ast'; +import {Identifiers} from '../../../../render3/r3_identifiers'; +import * as ir from '../../ir'; +import {CompilationJob} from '../compilation'; +import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from '../namespaces'; + +/** + * Wraps static i18n extracted attributes in their corresponding sanitizers/validators. + */ +export function resolveI18nAttrSanitizers(job: CompilationJob): void { + const tagNamesByElement = new Map(); + + for (const unit of job.units) { + for (const op of unit.ops()) { + if (op.kind === ir.OpKind.ElementStart || op.kind === ir.OpKind.Template) { + let tag = op.tag ?? ''; + switch (op.namespace) { + case ir.Namespace.SVG: + tag = `:${SVG_NAMESPACE}:${tag}`; + break; + case ir.Namespace.Math: + tag = `:${MATH_ML_NAMESPACE}:${tag}`; + break; + } + + tagNamesByElement.set(op.xref, tag); + } + } + } + + for (const unit of job.units) { + for (const op of unit.create) { + if ( + op.kind === ir.OpKind.ExtractedAttribute && + op.i18nContext !== null && + op.expression !== null + ) { + const tagName = tagNamesByElement.get(op.target) ?? ''; + let expr = op.expression; + switch (op.securityContext) { + case SecurityContext.HTML: + expr = o.importExpr(Identifiers.sanitizeHtml).callFn([expr]); + break; + case SecurityContext.STYLE: + expr = o.importExpr(Identifiers.sanitizeStyle).callFn([expr]); + break; + case SecurityContext.SCRIPT: + expr = o.importExpr(Identifiers.sanitizeScript).callFn([expr]); + break; + case SecurityContext.URL: + expr = o.importExpr(Identifiers.sanitizeUrl).callFn([expr]); + break; + case SecurityContext.RESOURCE_URL: + expr = o.importExpr(Identifiers.sanitizeResourceUrl).callFn([expr]); + break; + case SecurityContext.ATTRIBUTE_NO_BINDING: + expr = o + .importExpr(Identifiers.validateAttribute) + .callFn([expr, o.literal(tagName), o.literal(op.name)]); + break; + } + op.expression = expr; + } + } + } +} diff --git a/packages/compiler/test/schema/schema_extractor.ts b/packages/compiler/test/schema/schema_extractor.ts deleted file mode 100644 index 8ddfc00db44b..000000000000 --- a/packages/compiler/test/schema/schema_extractor.ts +++ /dev/null @@ -1,370 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -const SVG_PREFIX = ':svg:'; -const MATH_PREFIX = ':math:'; - -// Element | Node interfaces -// see https://developer.mozilla.org/en-US/docs/Web/API/Element -// see https://developer.mozilla.org/en-US/docs/Web/API/Node -const ELEMENT_IF = '[Element]'; -// HTMLElement interface -// see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement -const HTMLELEMENT_IF = '[HTMLElement]'; - -const HTMLELEMENT_TAGS = - 'abbr,address,article,aside,b,bdi,bdo,cite,content,code,dd,dfn,dt,em,figcaption,figure,footer,header,hgroup,i,kbd,main,mark,nav,noscript,rb,rp,rt,rtc,ruby,s,samp,search,section,small,strong,sub,sup,u,var,wbr'; - -const ALL_HTML_TAGS = - // https://www.w3.org/TR/html5/index.html - 'a,abbr,address,area,article,aside,audio,b,base,bdi,bdo,blockquote,body,br,button,canvas,caption,cite,code,col,colgroup,content,data,datalist,dd,del,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,i,iframe,img,input,ins,kbd,keygen,label,legend,li,link,main,map,mark,meta,meter,nav,noscript,object,ol,optgroup,option,output,p,param,pre,progress,q,rb,rp,rt,rtc,ruby,s,samp,script,search,section,select,small,source,span,strong,style,sub,sup,table,tbody,td,template,textarea,tfoot,th,thead,time,title,tr,track,u,ul,var,video,wbr,' + - // https://html.spec.whatwg.org/ - 'details,summary,menu,menuitem,geolocation'; - -// Via https://developer.mozilla.org/en-US/docs/Web/MathML -const ALL_MATH_TAGS = - 'math,maction,menclose,merror,mfenced,mfrac,mi,mmultiscripts,mn,mo,mover,mpadded,mphantom,mroot,mrow,ms,mspace,msqrt,mstyle,msub,msubsup,msup,mtable,mtd,mtext,mtr,munder,munderover,semantics'; - -// Elements missing from Chrome (HtmlUnknownElement), to be manually added -const MISSING_FROM_CHROME: {[el: string]: string[]} = { - 'data^[HTMLElement]': ['value'], - 'keygen^[HTMLElement]': ['!autofocus', 'challenge', '!disabled', 'form', 'keytype', 'name'], - // TODO(vicb): Figure out why Chrome and WhatWG do not agree on the props - // 'menu^[HTMLElement]': ['type', 'label'], - 'menuitem^[HTMLElement]': [ - 'type', - 'label', - 'icon', - '!disabled', - '!checked', - 'radiogroup', - '!default', - ], - 'summary^[HTMLElement]': [], - 'time^[HTMLElement]': ['dateTime'], - ':svg:cursor^:svg:': [], -}; - -const _G: any = - (typeof window != 'undefined' && window) || - (typeof global != 'undefined' && global) || - (typeof self != 'undefined' && self); - -const document: any = typeof _G['document'] == 'object' ? _G['document'] : null; - -export function extractSchema(): Map | null { - if (!document) return null; - const SVGGraphicsElement = _G['SVGGraphicsElement']; - if (!SVGGraphicsElement) return null; - - const element = document.createElement('video'); - const descMap: Map = new Map(); - const visited: {[name: string]: boolean} = {}; - - // HTML top level - extractProperties(Node, element, visited, descMap, ELEMENT_IF, ''); - extractProperties(Element, element, visited, descMap, ELEMENT_IF, ''); - extractProperties(HTMLElement, element, visited, descMap, HTMLELEMENT_IF, ELEMENT_IF); - extractProperties(HTMLElement, element, visited, descMap, HTMLELEMENT_TAGS, HTMLELEMENT_IF); - extractProperties(HTMLMediaElement, element, visited, descMap, 'media', HTMLELEMENT_IF); - - // SVG top level - const svgAnimation = document.createElementNS('http://www.w3.org/2000/svg', 'set'); - const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - const svgFeFuncA = document.createElementNS('http://www.w3.org/2000/svg', 'feFuncA'); - const svgGradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient'); - const svgText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - - const SVGAnimationElement = _G['SVGAnimationElement']; - const SVGGeometryElement = _G['SVGGeometryElement']; - const SVGComponentTransferFunctionElement = _G['SVGComponentTransferFunctionElement']; - const SVGGradientElement = _G['SVGGradientElement']; - const SVGTextContentElement = _G['SVGTextContentElement']; - const SVGTextPositioningElement = _G['SVGTextPositioningElement']; - extractProperties(SVGElement, svgText, visited, descMap, SVG_PREFIX, HTMLELEMENT_IF); - - extractProperties( - SVGGraphicsElement, - svgText, - visited, - descMap, - SVG_PREFIX + 'graphics', - SVG_PREFIX, - ); - extractProperties( - SVGAnimationElement, - svgAnimation, - visited, - descMap, - SVG_PREFIX + 'animation', - SVG_PREFIX, - ); - extractProperties( - SVGGeometryElement, - svgPath, - visited, - descMap, - SVG_PREFIX + 'geometry', - SVG_PREFIX, - ); - extractProperties( - SVGComponentTransferFunctionElement, - svgFeFuncA, - visited, - descMap, - SVG_PREFIX + 'componentTransferFunction', - SVG_PREFIX, - ); - extractProperties( - SVGGradientElement, - svgGradient, - visited, - descMap, - SVG_PREFIX + 'gradient', - SVG_PREFIX, - ); - extractProperties( - SVGTextContentElement, - svgText, - visited, - descMap, - SVG_PREFIX + 'textContent', - SVG_PREFIX + 'graphics', - ); - extractProperties( - SVGTextPositioningElement, - svgText, - visited, - descMap, - SVG_PREFIX + 'textPositioning', - SVG_PREFIX + 'textContent', - ); - - // Get all element types - const types = Object.getOwnPropertyNames(window).filter((k) => /^(HTML|SVG).*?Element$/.test(k)); - - types.sort(); - - types.forEach((type) => { - extractRecursiveProperties(visited, descMap, (window as any)[type]); - }); - - // Add elements missed by Chrome auto-detection - Object.keys(MISSING_FROM_CHROME).forEach((elHierarchy) => { - descMap.set(elHierarchy, MISSING_FROM_CHROME[elHierarchy]); - }); - - // Needed because we're running tests against some older Android versions. - if (typeof MathMLElement !== 'undefined') { - // Math top level - const math = document.createElementNS('http://www.w3.org/1998/Math/MathML', 'math'); - extractProperties(MathMLElement, math, visited, descMap, MATH_PREFIX, HTMLELEMENT_IF); - - // This script is written under the assumption that each tag has a corresponding class name, e.g. - // `` -> `SVGCircleElement` however this doesn't hold for Math elements which are all - // `MathMLElement`. Furthermore, they don't have special property names, but rather are - // configured exclusively via attributes. Register them as plain elements that inherit from - // the top-level `:math` namespace. - ALL_MATH_TAGS.split(',').forEach((tag) => - descMap.set(`${MATH_PREFIX}${tag}^${MATH_PREFIX}`, []), - ); - } - - assertNoMissingTags(descMap); - - return descMap; -} - -function assertNoMissingTags(descMap: Map): void { - const extractedTags: string[] = []; - - Array.from(descMap.keys()).forEach((key: string) => { - extractedTags.push(...key.split('|')[0].split('^')[0].split(',')); - }); - - const missingTags = [ - ...ALL_HTML_TAGS.split(','), - ...(typeof MathMLElement === 'undefined' - ? [] - : ALL_MATH_TAGS.split(',').map((tag) => MATH_PREFIX + tag)), - ].filter((tag) => !extractedTags.includes(tag)); - - if (missingTags.length) { - throw new Error(`DOM schema misses tags: ${missingTags.join(',')}`); - } -} - -function extractRecursiveProperties( - visited: {[name: string]: boolean}, - descMap: Map, - type: Function, -): string { - const name = extractName(type)!; - - if (visited[name]) { - return name; - } - - let superName: string; - switch (name) { - case ELEMENT_IF: - // ELEMENT_IF is the top most interface (Element | Node) - superName = ''; - break; - case HTMLELEMENT_IF: - superName = ELEMENT_IF; - break; - default: - superName = extractRecursiveProperties( - visited, - descMap, - type.prototype.__proto__.constructor, - ); - } - - let instance: HTMLElement | null = null; - name.split(',').forEach((tagName) => { - instance = type['name'].startsWith('SVG') - ? document.createElementNS('http://www.w3.org/2000/svg', tagName.replace(SVG_PREFIX, '')) - : document.createElement(tagName); - - let htmlType: Function; - - switch (tagName) { - case 'cite': - // interface is `HTMLQuoteElement` - htmlType = HTMLElement; - break; - default: - htmlType = type; - } - - if (!(instance instanceof htmlType)) { - throw new Error(`Tag <${tagName}> is not an instance of ${htmlType['name']}`); - } - }); - - extractProperties(type, instance, visited, descMap, name, superName); - - return name; -} - -function extractProperties( - type: Function, - instance: any, - visited: {[name: string]: boolean}, - descMap: Map, - name: string, - superName: string, -) { - if (!type) return; - - visited[name] = true; - - const fullName = name + (superName ? '^' + superName : ''); - - const props: string[] = descMap.has(fullName) ? descMap.get(fullName)! : []; - - const prototype = type.prototype; - const keys = Object.getOwnPropertyNames(prototype); - - keys.sort(); - keys.forEach((name) => { - if (name.startsWith('on')) { - props.push('*' + name.slice(2)); - } else { - const typeCh = _TYPE_MNEMONICS[typeof instance[name]]; - const descriptor = Object.getOwnPropertyDescriptor(prototype, name); - const isSetter = descriptor && descriptor.set; - if (typeCh !== void 0 && !name.startsWith('webkit') && isSetter) { - props.push(typeCh + name); - } - } - }); - - // There is no point in using `Node.nodeValue`, filter it out - descMap.set(fullName, type === Node ? props.filter((p) => p != '%nodeValue') : props); -} - -function extractName(type: Function): string | null { - let name = type['name']; - - // The polyfill @webcomponents/custom-element/src/native-shim.js overrides the - // window.HTMLElement and does not have the name property. Check if this is the - // case and if so, set the name manually. - if (name === '' && type === HTMLElement) { - name = 'HTMLElement'; - } - - switch (name) { - // see https://www.w3.org/TR/html5/index.html - // TODO(vicb): generate this map from all the element types - case 'Element': - return ELEMENT_IF; - case 'HTMLElement': - return HTMLELEMENT_IF; - case 'HTMLImageElement': - return 'img'; - case 'HTMLAnchorElement': - return 'a'; - case 'HTMLDListElement': - return 'dl'; - case 'HTMLDirectoryElement': - return 'dir'; - case 'HTMLHeadingElement': - return 'h1,h2,h3,h4,h5,h6'; - case 'HTMLModElement': - return 'ins,del'; - case 'HTMLOListElement': - return 'ol'; - case 'HTMLParagraphElement': - return 'p'; - case 'HTMLQuoteElement': - return 'q,blockquote,cite'; - case 'HTMLTableCaptionElement': - return 'caption'; - case 'HTMLTableCellElement': - return 'th,td'; - case 'HTMLTableColElement': - return 'col,colgroup'; - case 'HTMLTableRowElement': - return 'tr'; - case 'HTMLTableSectionElement': - return 'tfoot,thead,tbody'; - case 'HTMLUListElement': - return 'ul'; - case 'SVGGraphicsElement': - return SVG_PREFIX + 'graphics'; - case 'SVGMPathElement': - return SVG_PREFIX + 'mpath'; - case 'SVGSVGElement': - return SVG_PREFIX + 'svg'; - case 'SVGTSpanElement': - return SVG_PREFIX + 'tspan'; - default: - const isSVG = name.startsWith('SVG'); - if (name.startsWith('HTML') || isSVG) { - name = name.replace('HTML', '').replace('SVG', '').replace('Element', ''); - if (isSVG && name.startsWith('FE')) { - name = 'fe' + name.substring(2); - } else if (name) { - name = name.charAt(0).toLowerCase() + name.substring(1); - } - return isSVG ? SVG_PREFIX + name : name.toLowerCase(); - } - } - - return null; -} - -const _TYPE_MNEMONICS: {[type: string]: string} = { - 'string': '', - 'number': '#', - 'boolean': '!', - 'object': '%', -}; diff --git a/packages/core/BUILD.bazel b/packages/core/BUILD.bazel index c6f5576c0c43..4eeeaf056630 100644 --- a/packages/core/BUILD.bazel +++ b/packages/core/BUILD.bazel @@ -1,3 +1,4 @@ +load("@bazel_lib//lib:write_source_files.bzl", "write_source_file") load("@npm//:defs.bzl", "npm_link_all_packages") load("//adev/shared-docs/pipeline/api-gen:generate_api_docs.bzl", "generate_api_docs") load("//packages/common/locales:index.bzl", "generate_base_locale_file") @@ -165,3 +166,10 @@ genrule( outs = ["event-dispatch-contract.min.js"], cmd = "cp $< $@", ) + +write_source_file( + name = "dom_security_schema", + check_that_out_file_exists = False, + in_file = "//packages/compiler:src/schema/dom_security_schema.ts", + out_file = ":src/sanitization/dom_security_schema.ts", +) diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index d934eefdf4c4..054cac34be1d 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -115,7 +115,7 @@ export {createEnvironmentInjector, createNgModule} from './render3/ng_module_ref export {publishExternalGlobalUtil as ɵpublishExternalGlobalUtil} from './render3/util/global_utils'; export * from './resource'; export {Sanitizer} from './sanitization/sanitizer'; -export {SecurityContext} from './sanitization/security'; +export {SecurityContext} from './sanitization/dom_security_schema'; export { GetTestability, setTestabilityGetter, diff --git a/packages/core/src/render3/i18n/i18n_apply.ts b/packages/core/src/render3/i18n/i18n_apply.ts index b2e160541972..96745d19fbb6 100644 --- a/packages/core/src/render3/i18n/i18n_apply.ts +++ b/packages/core/src/render3/i18n/i18n_apply.ts @@ -49,8 +49,10 @@ import { } from '../dom_node_manipulation'; import { getBindingIndex, + getSelectedIndex, isInSkipHydrationBlock, lastNodeWasCreated, + setSelectedIndex, wasLastNodeCreated, } from '../state'; import {renderStringify} from '../util/stringify_utils'; @@ -442,14 +444,20 @@ function applyUpdateOpCodes( sanitizeFn, ); } else { - setPropertyAndInputs( - tNodeOrTagName, - lView, - propName, - value, - lView[RENDERER], - sanitizeFn, - ); + const prevSelectedIndex = getSelectedIndex(); + setSelectedIndex(nodeIndex); + try { + setPropertyAndInputs( + tNodeOrTagName, + lView, + propName, + value, + lView[RENDERER], + sanitizeFn, + ); + } finally { + setSelectedIndex(prevSelectedIndex); + } } break; case I18nUpdateOpCode.Text: diff --git a/packages/core/src/render3/i18n/i18n_parse.ts b/packages/core/src/render3/i18n/i18n_parse.ts index 5459e0a920d5..28a8441ea60b 100644 --- a/packages/core/src/render3/i18n/i18n_parse.ts +++ b/packages/core/src/render3/i18n/i18n_parse.ts @@ -9,18 +9,17 @@ import '../../util/ng_dev_mode'; import '../../util/ng_i18n_closure_mode'; import {XSS_SECURITY_URL} from '../../error_details_base_url'; -import { - getTemplateContent, - SENSITIVE_ATTRS, - VALID_ATTRS, - VALID_ELEMENTS, -} from '../../sanitization/html_sanitizer'; +import {getTemplateContent, VALID_ATTRS, VALID_ELEMENTS} from '../../sanitization/html_sanitizer'; import {getInertBodyHelper} from '../../sanitization/inert_body'; import {_sanitizeUrl} from '../../sanitization/url_sanitizer'; import { + ɵɵsanitizeHtml as _sanitizeHtml, + ɵɵsanitizeStyle as _sanitizeStyle, + ɵɵsanitizeScript as _sanitizeScript, + ɵɵsanitizeResourceUrl as _sanitizeResourceUrl, ɵɵvalidateAttribute as _validateAttribute, - SECURITY_SENSITIVE_ELEMENTS, } from '../../sanitization/sanitization'; +import {SECURITY_SCHEMA, SecurityContext} from '../../sanitization/dom_security_schema'; import { assertDefined, assertEqual, @@ -386,13 +385,16 @@ export function i18nAttributesFirstPass(tView: TView, index: number, values: str // the compiler treats static i18n attributes as regular attribute bindings. // Since this may not be the first i18n attribute on this element we need to pass in how // many previous bindings there have already been. + const tagName = previousElement.namespace + ? `:${previousElement.namespace}:${previousElement.value}` + : previousElement.value; generateBindingUpdateOpCodes( updateOpCodes, message, previousElementIndex, attrName, countBindings(updateOpCodes), - i18nSanitizeAttribute(attrName), + i18nResolveSanitizer(attrName, tagName), ); } } @@ -812,6 +814,13 @@ function walkIcuTree( const attr = elAttrs.item(i)!; const lowerAttrName = attr.name.toLowerCase(); const hasBinding = !!attr.value.match(BINDING_REGEXP); + const elementNS = element.namespaceURI; + const tagNameWithNamespace = + elementNS === 'http://www.w3.org/2000/svg' + ? `:svg:${tagName}` + : elementNS === 'http://www.w3.org/1998/Math/MathML' + ? `:math:${tagName}` + : tagName; if (hasBinding) { if (VALID_ATTRS.hasOwnProperty(lowerAttrName)) { generateBindingUpdateOpCodes( @@ -820,7 +829,7 @@ function walkIcuTree( newIndex, attr.name, 0, - i18nSanitizeAttribute(lowerAttrName), + i18nResolveSanitizer(lowerAttrName, tagNameWithNamespace), ); } else { ngDevMode && @@ -831,9 +840,9 @@ function walkIcuTree( ); } } else if (VALID_ATTRS[lowerAttrName]) { - if (SENSITIVE_ATTRS[lowerAttrName]) { - // Don't sanitize, because no value is acceptable in sensitive attributes. - // Translators are not allowed to create URIs. + let val = attr.value; + const sanitizer = i18nResolveSanitizer(lowerAttrName, tagNameWithNamespace); + if (sanitizer) { if (typeof ngDevMode !== 'undefined' && ngDevMode) { console.warn( `WARNING: ignoring unsafe attribute ` + @@ -841,9 +850,10 @@ function walkIcuTree( `(see ${XSS_SECURITY_URL})`, ); } + addCreateAttribute(create, newIndex, attr.name, 'unsafe:blocked'); } else { - addCreateAttribute(create, newIndex, attr.name, attr.value); + addCreateAttribute(create, newIndex, attr.name, val); } } else { if (typeof ngDevMode !== 'undefined' && ngDevMode) { @@ -974,32 +984,29 @@ function addCreateAttribute( create.push((newIndex << IcuCreateOpCode.SHIFT_REF) | IcuCreateOpCode.Attr, attrName, attrValue); } -/** - * Caches all keys of `SECURITY_SENSITIVE_ELEMENTS` in a Set to avoid recomputing - * or scanning them on every invocation. - */ -const SECURITY_SENSITIVE_ATTRS: ReadonlySet = /* @__PURE__ */ (() => - new Set( - Object.values(SECURITY_SENSITIVE_ELEMENTS).flatMap((attrs) => - attrs ? Object.keys(attrs) : [], - ), - ))(); - -/** - * Returns a sanitizer for the given attribute name or null if the attribute is not security sensitive. - * - * @param attrName The name of the attribute to sanitize. - * @returns The sanitizer for the given attribute name. - */ -function i18nSanitizeAttribute(attrName: string): SanitizerFn | null { +function i18nResolveSanitizer(attrName: string, tagName?: string): SanitizerFn | null { const lowerAttrName = attrName.toLowerCase(); - if (SENSITIVE_ATTRS[lowerAttrName]) { - return _sanitizeUrl; + const lowerTagName = tagName ? tagName.toLowerCase() : '*'; + const schema = SECURITY_SCHEMA(); + const schemaContext = + schema[`${lowerTagName}|${lowerAttrName}`] || + schema[`*|${lowerAttrName}`] || + SecurityContext.NONE; + + switch (schemaContext) { + case SecurityContext.HTML: + return _sanitizeHtml; + case SecurityContext.STYLE: + return _sanitizeStyle; + case SecurityContext.SCRIPT: + return _sanitizeScript; + case SecurityContext.URL: + return _sanitizeUrl; + case SecurityContext.RESOURCE_URL: + return _sanitizeResourceUrl; + case SecurityContext.ATTRIBUTE_NO_BINDING: + return _validateAttribute; + default: + return null; } - - if (SECURITY_SENSITIVE_ATTRS.has(lowerAttrName)) { - return _validateAttribute; - } - - return null; } diff --git a/packages/core/src/sanitization/dom_security_schema.ts b/packages/core/src/sanitization/dom_security_schema.ts new file mode 100644 index 000000000000..179752e79f79 --- /dev/null +++ b/packages/core/src/sanitization/dom_security_schema.ts @@ -0,0 +1,176 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +/** + * A SecurityContext marks a location that has dangerous security implications, e.g. a DOM property + * like `innerHTML` that could cause Cross Site Scripting (XSS) security bugs when improperly + * handled. + * + * See DomSanitizer for more details on security in Angular applications. + * + * @publicApi + */ +export enum SecurityContext { + NONE = 0, + HTML = 1, + STYLE = 2, + SCRIPT = 3, + URL = 4, + RESOURCE_URL = 5, + ATTRIBUTE_NO_BINDING = 6, +} + +// ================================================================================================= +// ================================================================================================= +// =========== S T O P - S T O P - S T O P - S T O P - S T O P - S T O P =========== +// ================================================================================================= +// ================================================================================================= +// +// DO NOT EDIT THIS LIST OF SECURITY SENSITIVE PROPERTIES WITHOUT A SECURITY REVIEW! +// +// ================================================================================================= + +/** + * Map from tagName|propertyName to SecurityContext. Properties applying to all tags use '*'. + */ +let _SECURITY_SCHEMA!: {[k: string]: SecurityContext}; +const SVG_NAMESPACE = 'svg'; +const MATH_ML_NAMESPACE = 'math'; + +/** + * @remarks Keep is a copy of DOM Security Schema. + * @see [SECURITY_SCHEMA](../../../compiler/src/schema/dom_security_schema.ts) + */ +export function SECURITY_SCHEMA(): {[k: string]: SecurityContext} { + if (!_SECURITY_SCHEMA) { + _SECURITY_SCHEMA = {}; + // Case is insignificant below, all element and attribute names are lower-cased for lookup. + + registerContext(SecurityContext.HTML, /** Namespace */ undefined, [ + ['iframe', ['srcdoc']], + ['*', ['innerHTML', 'outerHTML']], + ]); + registerContext(SecurityContext.STYLE, /** Namespace */ undefined, [['*', ['style']]]); + // NB: no SCRIPT contexts here, they are never allowed due to the parser stripping them. + registerContext(SecurityContext.URL, /** Namespace */ undefined, [ + ['*', ['formAction']], + ['area', ['href']], + ['a', ['href', 'xlink:href']], + ['form', ['action']], + + // The below two items are safe and should be removed but they require a G3 clean-up as a small number of tests fail. + ['img', ['src']], + ['video', ['src']], + ]); + + registerContext(SecurityContext.URL, MATH_ML_NAMESPACE, [ + // MathML namespace + // https://crsrc.org/c/third_party/blink/renderer/core/sanitizer/sanitizer.cc;l=753-768;drc=b3eb16372dcd3317d65e9e0265015e322494edcd;bpv=1;bpt=1 + ['annotation', ['href', 'xlink:href']], + ['annotation-xml', ['href', 'xlink:href']], + ['maction', ['href', 'xlink:href']], + ['malignmark', ['href', 'xlink:href']], + ['math', ['href', 'xlink:href']], + ['mroot', ['href', 'xlink:href']], + ['msqrt', ['href', 'xlink:href']], + ['merror', ['href', 'xlink:href']], + ['mfrac', ['href', 'xlink:href']], + ['mglyph', ['href', 'xlink:href']], + ['msub', ['href', 'xlink:href']], + ['msup', ['href', 'xlink:href']], + ['msubsup', ['href', 'xlink:href']], + ['mmultiscripts', ['href', 'xlink:href']], + ['mprescripts', ['href', 'xlink:href']], + ['mi', ['href', 'xlink:href']], + ['mn', ['href', 'xlink:href']], + ['mo', ['href', 'xlink:href']], + ['mpadded', ['href', 'xlink:href']], + ['mphantom', ['href', 'xlink:href']], + ['mrow', ['href', 'xlink:href']], + ['ms', ['href', 'xlink:href']], + ['mspace', ['href', 'xlink:href']], + ['mstyle', ['href', 'xlink:href']], + ['mtable', ['href', 'xlink:href']], + ['mtd', ['href', 'xlink:href']], + ['mtr', ['href', 'xlink:href']], + ['mtext', ['href', 'xlink:href']], + ['mover', ['href', 'xlink:href']], + ['munder', ['href', 'xlink:href']], + ['munderover', ['href', 'xlink:href']], + ['semantics', ['href', 'xlink:href']], + ['none', ['href', 'xlink:href']], + ]); + + registerContext(SecurityContext.RESOURCE_URL, /** Namespace */ undefined, [ + ['base', ['href']], + ['embed', ['src']], + ['frame', ['src']], + ['iframe', ['src']], + ['link', ['href']], + ['object', ['codebase', 'data']], + ]); + + // The below are for Script SVG + // See: https://developer.mozilla.org/en-US/docs/Web/API/SVGScriptElement/href + registerContext(SecurityContext.RESOURCE_URL, SVG_NAMESPACE, [ + ['script', ['src', 'href', 'xlink:href']], + ]); + + // Keep this in sync with SECURITY_SENSITIVE_ELEMENTS in packages/core/src/sanitization/sanitization.ts + // The `unknown` elements refer to cases when we need to validate the input/binding in a directive (host bindings) + // and the directive can be applied to multiple different elements (with different tag names). In this case we generate + // a special instruction that an attribute might potentially be security-sensitive and defer the actual security check + // to runtime, when we apply that directive to a concrete elements, thus we can check the combination of tag+attribute + // against the set that requires sanitization. + // These are unsafe as `attributeName` can be `href` or `xlink:href` + // See: http://b/463880509#comment7 + registerContext(SecurityContext.ATTRIBUTE_NO_BINDING, SVG_NAMESPACE, [ + ['animate', ['attributeName', 'values', 'to', 'from']], + ['set', ['to', 'attributeName']], + ['animateMotion', ['attributeName']], + ['animateTransform', ['attributeName']], + ]); + + registerContext(SecurityContext.ATTRIBUTE_NO_BINDING, /** Namespace */ undefined, [ + [ + 'unknown', + [ + 'attributeName', + 'values', + 'to', + 'from', + 'sandbox', + 'allow', + 'allowFullscreen', + 'referrerPolicy', + 'csp', + 'fetchPriority', + ], + ], + ['iframe', ['sandbox', 'allow', 'allowFullscreen', 'referrerPolicy', 'csp', 'fetchPriority']], + ]); + } + + return _SECURITY_SCHEMA; +} + +function registerContext( + ctx: SecurityContext, + namespace: string | undefined, + specs: readonly [tagName: string, attributeNames: readonly string[]][], +): void { + for (const [element, attributeNames] of specs) { + let tagName = + namespace && element !== '*' && element !== 'unknown' ? `:${namespace}:${element}` : element; + tagName = tagName.toLowerCase(); + + for (const attr of attributeNames) { + _SECURITY_SCHEMA[`${tagName}|${attr.toLowerCase()}`] = ctx; + } + } +} diff --git a/packages/core/src/sanitization/html_sanitizer.ts b/packages/core/src/sanitization/html_sanitizer.ts index 1238ea8bf531..8605775e9275 100644 --- a/packages/core/src/sanitization/html_sanitizer.ts +++ b/packages/core/src/sanitization/html_sanitizer.ts @@ -116,16 +116,6 @@ export const VALID_ATTRS: BooleanRecord = merge(URI_ATTRS, HTML_ATTRS, ARIA_ATTR // don't want to preserve the content, if the elements themselves are going to be removed. const SKIP_TRAVERSING_CONTENT_IF_INVALID_ELEMENTS = tagSet('script,style,template'); -/** - * Attributes that are potential attach vectors and may need to be sanitized. - */ -export const SENSITIVE_ATTRS: BooleanRecord = merge( - URI_ATTRS, - // Note: we don't include these attributes in `URI_ATTRS`, because `URI_ATTRS` also - // determines whether an attribute should be dropped when sanitizing an HTML string. - tagSet('action,formaction,data,codebase'), -); - /** * SanitizingHtmlSerializer serializes a DOM fragment, stripping out any unsafe elements and unsafe * attributes. diff --git a/packages/core/src/sanitization/sanitization.ts b/packages/core/src/sanitization/sanitization.ts index 9b0c6b54063d..58f3ad3d42a8 100644 --- a/packages/core/src/sanitization/sanitization.ts +++ b/packages/core/src/sanitization/sanitization.ts @@ -10,10 +10,10 @@ import {XSS_SECURITY_URL} from '../error_details_base_url'; import {RuntimeError, RuntimeErrorCode} from '../errors'; import {getTemplateLocationDetails} from '../render3/instructions/element_validation'; import {getDocument} from '../render3/interfaces/document'; -import {TNodeType} from '../render3/interfaces/node'; +import {TNode, TNodeType} from '../render3/interfaces/node'; import {RElement} from '../render3/interfaces/renderer_dom'; import {ENVIRONMENT} from '../render3/interfaces/view'; -import {getLView, getSelectedTNode} from '../render3/state'; +import {getLView, getSelectedIndex, getSelectedTNode} from '../render3/state'; import {renderStringify} from '../render3/util/stringify_utils'; import {getNativeByTNode} from '../render3/util/view_utils'; import {TrustedHTML, TrustedScript, TrustedScriptURL} from '../util/security/trusted_type_defs'; @@ -25,11 +25,11 @@ import { } from '../util/security/trusted_types_bypass'; import {allowSanitizationBypassAndThrow, BypassType, unwrapSafeValue} from './bypass'; -import {_sanitizeHtml as _sanitizeHtml} from './html_sanitizer'; +import {_sanitizeHtml} from './html_sanitizer'; import {enforceIframeSecurity} from './iframe_attrs_validation'; import {Sanitizer} from './sanitizer'; -import {SecurityContext} from './security'; -import {_sanitizeUrl as _sanitizeUrl} from './url_sanitizer'; +import {SecurityContext} from './dom_security_schema'; +import {_sanitizeUrl} from './url_sanitizer'; /** * An `html` sanitizer which converts untrusted `html` **string** into trusted string by removing @@ -283,7 +283,7 @@ const SECURITY_SENSITIVE_ATTRIBUTE_NAMES: ReadonlySet = new Set(['href', * @remarks Keep this in sync with DOM Security Schema. * @see [SECURITY_SCHEMA](../../../compiler/src/schema/dom_security_schema.ts) */ -export const SECURITY_SENSITIVE_ELEMENTS: Record< +const SECURITY_SENSITIVE_ELEMENTS: Record< string, Record> | undefined > = { @@ -317,8 +317,13 @@ export function ɵɵvalidateAttribute(value: T, tagName: string, attrib const lowerCaseTagName = tagName.toLowerCase(); const lowerCaseAttrName = attributeName.toLowerCase(); + const index = getSelectedIndex(); + const tNode: TNode | null = index === -1 ? null : getSelectedTNode(); + if (tNode && tNode.type !== TNodeType.Element) { + return value; + } + // Leverage tNode.namespace if active, otherwise check both namespaced and base variants. - const tNode = getSelectedTNode(); const fullTagName = lowerCaseTagName[0] !== ':' && tNode?.namespace ? `:${tNode.namespace}:${lowerCaseTagName}` @@ -331,32 +336,40 @@ export function ɵɵvalidateAttribute(value: T, tagName: string, attrib } const lView = getLView(); - if (lowerCaseTagName === 'iframe') { - if (tNode?.type === TNodeType.Element) { - const element = getNativeByTNode(tNode, lView) as RElement; - enforceIframeSecurity(element as HTMLIFrameElement); - } + if (tNode && lowerCaseTagName === 'iframe') { + const element = getNativeByTNode(tNode, lView) as RElement; + enforceIframeSecurity(element as HTMLIFrameElement); } const displayTagName = tagName[0] === ':' ? tagName.split(':').pop()! : tagName; if (typeof validationConfig !== 'boolean') { - if (tNode?.type === TNodeType.Element) { - const element = getNativeByTNode(tNode, lView) as SVGAnimateElement; - const attributeNameValue = element.getAttribute('attributeName'); + if (!tNode) { + const errorMessage = + ngDevMode && + `Angular has detected that the \`${attributeName}\` was applied ` + + `as a binding to the <${tagName}> element. ` + + `For security reasons, the \`${attributeName}\` can be set on the <${tagName}> element ` + + `as a static attribute only. \n` + + `To fix this, switch the \`${attributeName}\` binding to a static attribute ` + + `in a template or in host bindings section.`; + throw new RuntimeError(RuntimeErrorCode.UNSAFE_ATTRIBUTE_BINDING, errorMessage); + } + + const element = getNativeByTNode(tNode, lView) as SVGAnimateElement; + const attributeNameValue = element.getAttribute('attributeName'); - if (attributeNameValue && validationConfig.has(attributeNameValue.toLowerCase())) { - const errorMessage = - ngDevMode && - `Angular has detected that the \`${attributeName}\` was applied ` + - `as a binding to the <${displayTagName}> element${getTemplateLocationDetails(lView)}. ` + - `For security reasons, the \`${attributeName}\` can be set on the <${displayTagName}> element ` + - `as a static attribute only when the "attributeName" is set to \'${attributeNameValue}\'. \n` + - `To fix this, switch the \`${attributeNameValue}\` binding to a static attribute ` + - `in a template or in host bindings section.`; + if (attributeNameValue && validationConfig.has(attributeNameValue.toLowerCase())) { + const errorMessage = + ngDevMode && + `Angular has detected that the \`${attributeName}\` was applied ` + + `as a binding to the <${displayTagName}> element${getTemplateLocationDetails(lView)}. ` + + `For security reasons, the \`${attributeName}\` can be set on the <${displayTagName}> element ` + + `as a static attribute only when the "attributeName" is set to \'${attributeNameValue}\'. \n` + + `To fix this, switch the \`${attributeNameValue}\` binding to a static attribute ` + + `in a template or in host bindings section.`; - throw new RuntimeError(RuntimeErrorCode.UNSAFE_ATTRIBUTE_BINDING, errorMessage); - } + throw new RuntimeError(RuntimeErrorCode.UNSAFE_ATTRIBUTE_BINDING, errorMessage); } return value; @@ -365,7 +378,7 @@ export function ɵɵvalidateAttribute(value: T, tagName: string, attrib const errorMessage = ngDevMode && `Angular has detected that the \`${attributeName}\` was applied ` + - `as a binding to the <${displayTagName}> element${getTemplateLocationDetails(lView)}. ` + + `as a binding to the <${displayTagName}> element${tNode ? getTemplateLocationDetails(lView) : ''}. ` + `For security reasons, the \`${attributeName}\` can be set on the <${displayTagName}> element ` + `as a static attribute only. \n` + `To fix this, switch the \`${attributeName}\` binding to a static attribute ` + diff --git a/packages/core/src/sanitization/sanitizer.ts b/packages/core/src/sanitization/sanitizer.ts index af51883f85ae..1488c96a4160 100644 --- a/packages/core/src/sanitization/sanitizer.ts +++ b/packages/core/src/sanitization/sanitizer.ts @@ -7,7 +7,7 @@ */ import {ɵɵdefineInjectable} from '../di/interface/defs'; -import {SecurityContext} from './security'; +import {SecurityContext} from './dom_security_schema'; /** * Sanitizer is used by the views to sanitize potentially dangerous values. diff --git a/packages/core/src/sanitization/security.ts b/packages/core/src/sanitization/security.ts deleted file mode 100644 index 87e72bcd532f..000000000000 --- a/packages/core/src/sanitization/security.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -/** - * A SecurityContext marks a location that has dangerous security implications, e.g. a DOM property - * like `innerHTML` that could cause Cross Site Scripting (XSS) security bugs when improperly - * handled. - * - * See DomSanitizer for more details on security in Angular applications. - * - * @publicApi - */ -export enum SecurityContext { - NONE = 0, - HTML = 1, - STYLE = 2, - SCRIPT = 3, - URL = 4, - RESOURCE_URL = 5, -} diff --git a/packages/core/test/linker/security_integration_spec.ts b/packages/core/test/linker/security_integration_spec.ts index 968501c3b384..bd01ac1a3feb 100644 --- a/packages/core/test/linker/security_integration_spec.ts +++ b/packages/core/test/linker/security_integration_spec.ts @@ -7,6 +7,8 @@ */ import {DomSanitizer} from '@angular/platform-browser'; +import {clearTranslations, loadTranslations} from '@angular/localize'; +import {computeMsgId} from '@angular/compiler'; import { ChangeDetectionStrategy, Component, @@ -282,6 +284,83 @@ describe('security integration tests', function () { }); describe('translation', () => { + afterEach(() => { + clearTranslations(); + }); + + it('should throw error on translated SVG script ResourceURL attributes', () => { + const template = ` + + + + `; + TestBed.overrideComponent(SecuredComponent, {set: {template}}); + + expect(() => TestBed.createComponent(SecuredComponent)).toThrowError( + /unsafe value used in a resource URL context/i, + ); + }); + + it('should throw error on SVG animation retargeting attributes', () => { + const template = ` + + + + + + + `; + TestBed.overrideComponent(SecuredComponent, {set: {template}}); + + expect(() => { + const fixture = TestBed.createComponent(SecuredComponent); + fixture.detectChanges(); + }).toThrowError( + /For security reasons, the `attributeName` can be set on the element as a static attribute only/i, + ); + }); + + it('should allow non-security sensitive attributes', () => { + loadTranslations({[computeMsgId('foo')]: 'bar'}); + const template = ``; + TestBed.overrideComponent(SecuredComponent, {set: {template}}); + + const fixture = TestBed.createComponent(SecuredComponent); + fixture.detectChanges(); + const element = fixture.nativeElement.querySelector('iframe'); + expect(element.getAttribute('title')).toEqual('bar'); + }); + + it('should sanitize translations of static iframe attributes', () => { + const template = ``; + TestBed.overrideComponent(SecuredComponent, {set: {template}}); + + expect(() => { + const fixture = TestBed.createComponent(SecuredComponent); + fixture.detectChanges(); + }).toThrowError( + /For security reasons, the `sandbox` can be set on the `; TestBed.overrideComponent(SecuredComponent, {set: {template}}); diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index 3cb2ebe1bb88..7edd0be76634 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -37,7 +37,7 @@ import { ɵɵsanitizeUrl, } from '../../src/sanitization/sanitization'; import {Sanitizer} from '../../src/sanitization/sanitizer'; -import {SecurityContext} from '../../src/sanitization/security'; +import {SecurityContext} from '../../src/sanitization/dom_security_schema'; import {ViewFixture} from './view_fixture'; diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 3af84c013f60..4ad749ab7a5f 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -19,7 +19,7 @@ import {TestBed} from '../../testing'; import {getLContext, readPatchedData} from '../../src/render3/context_discovery'; import {CONTEXT, HEADER_OFFSET} from '../../src/render3/interfaces/view'; import {Sanitizer} from '../../src/sanitization/sanitizer'; -import {SecurityContext} from '../../src/sanitization/security'; +import {SecurityContext} from '../../src/sanitization/dom_security_schema'; describe('element discovery', () => { it('should only monkey-patch immediate child nodes in a component', () => { diff --git a/packages/core/test/sanitization/sanitization_spec.ts b/packages/core/test/sanitization/sanitization_spec.ts index 6264d0159206..6558cfce647a 100644 --- a/packages/core/test/sanitization/sanitization_spec.ts +++ b/packages/core/test/sanitization/sanitization_spec.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import {SECURITY_SCHEMA} from '@angular/compiler'; import {ENVIRONMENT, LView} from '../../src/render3/interfaces/view'; import {enterView, leaveView} from '../../src/render3/state'; @@ -28,7 +27,7 @@ import { ɵɵtrustConstantHtml, ɵɵtrustConstantResourceUrl, } from '../../src/sanitization/sanitization'; -import {SecurityContext} from '../../src/sanitization/security'; +import {SECURITY_SCHEMA, SecurityContext} from '../../src/sanitization/dom_security_schema'; function fakeLView(): LView { const fake = [null, {}] as LView;