From 881134344e5cb88da3aa70321e9bda2ee993a938 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 11 Jun 2026 09:33:37 +0200 Subject: [PATCH 1/8] build: sanitize parsed comments Adds some logic to avoid accidental HTML injection through the Markdown renderer. --- .../markdown-to-html/docs-marked-renderer.mts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tools/markdown-to-html/docs-marked-renderer.mts b/tools/markdown-to-html/docs-marked-renderer.mts index 1432b9860f51..8589c6afada7 100644 --- a/tools/markdown-to-html/docs-marked-renderer.mts +++ b/tools/markdown-to-html/docs-marked-renderer.mts @@ -102,11 +102,11 @@ export class DocsMarkdownRenderer extends Renderer { file: string; region: string; }; - replacement = `
`; + replacement = `
`; } else { - replacement = `
`; + replacement = `
`; } return `${exampleStartMarker}${replacement}${exampleEndMarker}`; @@ -154,4 +154,17 @@ export class DocsMarkdownRenderer extends Renderer { return `${markdownOpen}${output}`; } + + private _escapeHtml(text: string): string { + if (!text) { + return text; + } + + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } } From d55c957c536c1b497c5e405951d7451fab15d336 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 11 Jun 2026 09:34:14 +0200 Subject: [PATCH 2/8] build: avoid command injection in golden script Avoids command injection in the API golden script by using `exec` instead of interpolating the entire command. --- scripts/approve-api-golden.mts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/approve-api-golden.mts b/scripts/approve-api-golden.mts index fd3a417c6fed..84a6964ca5a4 100644 --- a/scripts/approve-api-golden.mts +++ b/scripts/approve-api-golden.mts @@ -1,11 +1,11 @@ #!/usr/bin/env node import chalk from 'chalk'; +import {execFileSync} from 'child_process'; import {join} from 'path'; import sh from 'shelljs'; import {guessPackageName} from './util.mjs'; -const bazel = process.env['BAZEL'] || 'pnpm -s bazel'; const targetsToRun = new Set(); if (process.argv.length < 3) { @@ -34,5 +34,5 @@ for (const searchPackageName of process.argv.slice(2)) { } for (const target of targetsToRun) { - sh.exec(`${bazel} run ${target}`); + execFileSync('pnpm', ['-s', 'bazel', 'run', target], {stdio: 'inherit'}); } From ed39c02392c3e0fbd254f487e7fc42312e13b979 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 11 Jun 2026 09:35:27 +0200 Subject: [PATCH 3/8] build: avoid command injection when creating package archives Reworks the package archive script to avoid command injection by going through `exec` instead of constructing the command using string concatenation. --- scripts/create-package-archives.mjs | 33 ++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/scripts/create-package-archives.mjs b/scripts/create-package-archives.mjs index 537e0e782a05..8c7e93861d20 100755 --- a/scripts/create-package-archives.mjs +++ b/scripts/create-package-archives.mjs @@ -10,6 +10,7 @@ */ import {join} from 'path'; +import {execFileSync} from 'child_process'; import sh from 'shelljs'; import chalk from 'chalk'; import yargs from 'yargs'; @@ -40,16 +41,38 @@ const builtPackages = sh // If multiple packages should be archived, we also generate a single archive that // contains all packages. This makes it easier to transfer the release packages. if (builtPackages.length > 1) { - console.info('Creating archive with all packages..'); - sh.exec( - `tar --create --gzip --directory ${releasesDir} --file ${archivesDir}/all-${suffix}.tgz .`, + console.info('Creating archive with all packages...'); + execFileSync( + 'tar', + [ + '--create', + '--gzip', + '--directory', + releasesDir, + '--file', + `${archivesDir}/all-${suffix}.tgz`, + '.', + ], + {stdio: 'inherit'}, ); } +// Note that we're using `exec` here, rather than interpolating the arguments into `sh.exec`, +// to avoid a potential command injection through the `suffix` which is user-provided. for (const pkg of builtPackages) { console.info(`Creating archive for package: ${pkg.name}`); - sh.exec( - `tar --create --gzip --directory ${pkg.path} --file ${archivesDir}/${pkg.name}-${suffix}.tgz .`, + execFileSync( + 'tar', + [ + '--create', + '--gzip', + '--directory', + pkg.path, + '--file', + `${archivesDir}/${pkg.name}-${suffix}.tgz`, + '.', + ], + {stdio: 'inherit'}, ); } From 61896f5715dd1593ffde60901dcddffb41e29906 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 11 Jun 2026 09:36:13 +0200 Subject: [PATCH 4/8] refactor: avoid prototype pollution in keyboard manager Makes the check whether an input is a modifier more robust against prototype pollution. --- .../private/behaviors/event-manager/keyboard-event-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aria/private/behaviors/event-manager/keyboard-event-manager.ts b/src/aria/private/behaviors/event-manager/keyboard-event-manager.ts index 3685fc2e65d7..0b41a08c8f8d 100644 --- a/src/aria/private/behaviors/event-manager/keyboard-event-manager.ts +++ b/src/aria/private/behaviors/event-manager/keyboard-event-manager.ts @@ -60,7 +60,7 @@ export class KeyboardEventManager extends EventManager< } private _normalizeInputs(...args: unknown[]) { - const withModifiers = Array.isArray(args[0]) || (args[0] as string) in Modifier; + const withModifiers = Array.isArray(args[0]) || Modifier.hasOwnProperty(args[0] as string); const modifiers = withModifiers ? args[0] : Modifier.None; const key = withModifiers ? args[1] : args[0]; const handler = withModifiers ? args[2] : args[1]; From e14d353449e2509c1444c1bab8ef440297fa22df Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 11 Jun 2026 09:38:13 +0200 Subject: [PATCH 5/8] fix(cdk/layout): avoid CSS injection attacks in media matcher The media matcher needs to create a dummy stylesheet to work around some browser quirks. These changes ensure we don't accidentally inject malicious CSS into the page. --- src/cdk/layout/media-matcher.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/cdk/layout/media-matcher.ts b/src/cdk/layout/media-matcher.ts index 8459b90021be..cdce864b36e3 100644 --- a/src/cdk/layout/media-matcher.ts +++ b/src/cdk/layout/media-matcher.ts @@ -73,7 +73,12 @@ function createEmptyStyleRule(query: string, nonce: string | undefined | null) { } if (mediaQueryStyleNode.sheet) { - mediaQueryStyleNode.sheet.insertRule(`@media ${query} {body{ }}`, 0); + mediaQueryStyleNode.sheet.insertRule( + // Drop the curly braces to avoid injection attacks. Curly braces aren't + // valid media query syntax so this should be a no-op in valid cases. + `@media ${query.replace(/[{}]/g, '')} {body{ }}`, + 0, + ); mediaQueriesForWebkitCompatibility.add(query); } } catch (e) { From 0df00422f0a577c2b0f59e3dc2e514a40633bbb4 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 11 Jun 2026 09:39:29 +0200 Subject: [PATCH 6/8] fix(material/stepper): validate animation durations The `animationDuration` input is a potential CSS injection attack vector, because we pass the value directly along to the `animation-duration` binding. These changes mitigate the risk by validating the incoming value. --- src/material/stepper/stepper.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/material/stepper/stepper.ts b/src/material/stepper/stepper.ts index edf98e72b2db..394966d9968e 100644 --- a/src/material/stepper/stepper.ts +++ b/src/material/stepper/stepper.ts @@ -203,7 +203,13 @@ export class MatStepper extends CdkStepper implements AfterViewInit, AfterConten return this._animationDuration; } set animationDuration(value: string) { - this._animationDuration = /^\d+$/.test(value) ? value + 'ms' : value; + if (/^[0-9]+(?:\.[0-9]+)?$/.test(value)) { + this._animationDuration = value + 'ms'; + } else if (/^[0-9]+(?:\.[0-9]+)?(?:ms|s)$/.test(value)) { + this._animationDuration = value; + } else { + this._animationDuration = ''; + } } private _animationDuration = ''; From 3f6ea514fa640264c17ccd860b3f93224119c8aa Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 11 Jun 2026 09:40:13 +0200 Subject: [PATCH 7/8] fix(material/grid-list): always validate colspan We were dropping the `colspan` validation error in production mode which meant that it can go into an infinite loop. --- src/material/grid-list/tile-coordinator.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/material/grid-list/tile-coordinator.ts b/src/material/grid-list/tile-coordinator.ts index 14ebc161a39a..cbd84c0a73db 100644 --- a/src/material/grid-list/tile-coordinator.ts +++ b/src/material/grid-list/tile-coordinator.ts @@ -94,10 +94,9 @@ export class TileCoordinator { /** Finds the next available space large enough to fit the tile. */ private _findMatchingGap(tileCols: number): number { - if (tileCols > this.tracker.length && (typeof ngDevMode === 'undefined' || ngDevMode)) { + if (tileCols > this.tracker.length) { throw Error( - `mat-grid-list: tile with colspan ${tileCols} is wider than ` + - `grid with cols="${this.tracker.length}".`, + `mat-grid-list: tile with colspan ${tileCols} is wider than grid with cols="${this.tracker.length}".`, ); } From f15cb64d28d8218debd681df7bccb8493519fc74 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 11 Jun 2026 09:41:49 +0200 Subject: [PATCH 8/8] fix(material/bottom-sheet): ensure animation event comes from container Previous we were relying on the animation name to ensure that we only capture the correct animation. These changes harden it by also checking the event's `target`. --- goldens/material/bottom-sheet/index.api.md | 2 +- .../bottom-sheet/bottom-sheet-container.ts | 34 +++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/goldens/material/bottom-sheet/index.api.md b/goldens/material/bottom-sheet/index.api.md index 82c18018fbeb..1e99cde0cc4f 100644 --- a/goldens/material/bottom-sheet/index.api.md +++ b/goldens/material/bottom-sheet/index.api.md @@ -83,7 +83,7 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes enter(): void; exit(): void; // (undocumented) - protected _handleAnimationEvent(isStart: boolean, animationName: string): void; + protected _handleAnimationEvent(isStart: boolean, animationName: string, target: EventTarget | null): void; // (undocumented) ngOnDestroy(): void; // (undocumented) diff --git a/src/material/bottom-sheet/bottom-sheet-container.ts b/src/material/bottom-sheet/bottom-sheet-container.ts index 49bbc54d830f..93331d8df6e1 100644 --- a/src/material/bottom-sheet/bottom-sheet-container.ts +++ b/src/material/bottom-sheet/bottom-sheet-container.ts @@ -46,9 +46,9 @@ const EXIT_ANIMATION = '_mat-bottom-sheet-exit'; '[attr.role]': '_config.role', '[attr.aria-modal]': '_config.ariaModal', '[attr.aria-label]': '_config.ariaLabel', - '(animationstart)': '_handleAnimationEvent(true, $event.animationName)', - '(animationend)': '_handleAnimationEvent(false, $event.animationName)', - '(animationcancel)': '_handleAnimationEvent(false, $event.animationName)', + '(animationstart)': '_handleAnimationEvent(true, $event.animationName, $event.target)', + '(animationend)': '_handleAnimationEvent(false, $event.animationName, $event.target)', + '(animationcancel)': '_handleAnimationEvent(false, $event.animationName, $event.target)', }, imports: [CdkPortalOutlet], }) @@ -125,8 +125,8 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes private _simulateAnimation(name: typeof ENTER_ANIMATION | typeof EXIT_ANIMATION) { this._ngZone.run(() => { - this._handleAnimationEvent(true, name); - setTimeout(() => this._handleAnimationEvent(false, name)); + this._handleAnimationEvent(true, name, this._elementRef.nativeElement); + setTimeout(() => this._handleAnimationEvent(false, name, this._elementRef.nativeElement)); }); } @@ -139,15 +139,21 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes super._trapFocus({preventScroll: true}); } - protected _handleAnimationEvent(isStart: boolean, animationName: string) { - const isEnter = animationName === ENTER_ANIMATION; - const isExit = animationName === EXIT_ANIMATION; - - if (isEnter || isExit) { - this._animationStateChanged.emit({ - toState: isEnter ? 'visible' : 'hidden', - phase: isStart ? 'start' : 'done', - }); + protected _handleAnimationEvent( + isStart: boolean, + animationName: string, + target: EventTarget | null, + ) { + if (target === this._elementRef.nativeElement) { + const isEnter = animationName === ENTER_ANIMATION; + const isExit = animationName === EXIT_ANIMATION; + + if (isEnter || isExit) { + this._animationStateChanged.emit({ + toState: isEnter ? 'visible' : 'hidden', + phase: isStart ? 'start' : 'done', + }); + } } } }