From cb4a373270b363ee7334a67c17281bebf463d261 Mon Sep 17 00:00:00 2001 From: Herdiyan Adam Putra Date: Sat, 1 Aug 2026 02:06:29 +0700 Subject: [PATCH 1/2] fix(@schematics/angular): escape module specifier in insertImport insertImport builds an `import ... from ''` statement by interpolating fileName directly between single quotes with no escaping. A fileName containing a single quote breaks out of the string literal, so any caller that passes untrusted data as the module specifier (e.g. a schematic using addRootProvider's external() with a value sourced from user/project config) ends up inserting attacker-controlled source into the target file rather than a plain import path. fileName is now escaped for backslashes and single quotes before being interpolated, matching the existing single-quote output format so the common case (a real module specifier) is byte-for-byte unchanged. --- packages/schematics/angular/utility/ast-utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/schematics/angular/utility/ast-utils.ts b/packages/schematics/angular/utility/ast-utils.ts index da7fbf8f526c..e23122b3707a 100644 --- a/packages/schematics/angular/utility/ast-utils.ts +++ b/packages/schematics/angular/utility/ast-utils.ts @@ -77,9 +77,10 @@ export function insertImport( // if there are no imports or 'use strict' statement, insert import at beginning of file const insertAtBeginning = allImports.length === 0 && useStrict.length === 0; const separator = insertAtBeginning ? '' : `;${eol}`; + const escapedFileName = fileName.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); const toInsert = `${separator}import ${open}${importExpression}${close}` + - ` from '${fileName}'${insertAtBeginning ? `;${eol}` : ''}`; + ` from '${escapedFileName}'${insertAtBeginning ? `;${eol}` : ''}`; return insertAfterLastOccurrence( allImports, From 02bec13034a452b336c90f9a93ea7d05b46132e0 Mon Sep 17 00:00:00 2001 From: Herdiyan Adam Putra Date: Sat, 1 Aug 2026 02:18:45 +0700 Subject: [PATCH 2/2] address review: escape newline/carriage return, add unit tests Also escape \n and \r in the module specifier, since an unescaped newline would produce an unterminated string literal in the generated file. Adds two test cases covering quote/backslash and newline/ carriage-return escaping in insertImport's output. --- .../schematics/angular/utility/ast-utils.ts | 6 +++++- .../angular/utility/ast-utils_spec.ts | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/schematics/angular/utility/ast-utils.ts b/packages/schematics/angular/utility/ast-utils.ts index e23122b3707a..3324c5627cc1 100644 --- a/packages/schematics/angular/utility/ast-utils.ts +++ b/packages/schematics/angular/utility/ast-utils.ts @@ -77,7 +77,11 @@ export function insertImport( // if there are no imports or 'use strict' statement, insert import at beginning of file const insertAtBeginning = allImports.length === 0 && useStrict.length === 0; const separator = insertAtBeginning ? '' : `;${eol}`; - const escapedFileName = fileName.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + const escapedFileName = fileName + .replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r'); const toInsert = `${separator}import ${open}${importExpression}${close}` + ` from '${escapedFileName}'${insertAtBeginning ? `;${eol}` : ''}`; diff --git a/packages/schematics/angular/utility/ast-utils_spec.ts b/packages/schematics/angular/utility/ast-utils_spec.ts index c937d7124ebe..ead53d29d5b8 100644 --- a/packages/schematics/angular/utility/ast-utils_spec.ts +++ b/packages/schematics/angular/utility/ast-utils_spec.ts @@ -768,6 +768,24 @@ describe('ast utils', () => { expect(result).toBe(fileContent); }); + + it('should escape single quotes and backslashes in the module specifier', () => { + const fileContent = ''; + const source = getTsSource(filePath, fileContent); + const change = insertImport(source, filePath, 'Component', "foo'bar\\baz"); + const result = applyChanges(filePath, fileContent, [change]).trim(); + + expect(result).toBe("import { Component } from 'foo\\'bar\\\\baz';"); + }); + + it('should escape newlines and carriage returns in the module specifier', () => { + const fileContent = ''; + const source = getTsSource(filePath, fileContent); + const change = insertImport(source, filePath, 'Component', 'foo\nbar\rbaz'); + const result = applyChanges(filePath, fileContent, [change]).trim(); + + expect(result).toBe(`import { Component } from 'foo\\nbar\\rbaz';`); + }); }); describe('hasTopLevelIdentifier', () => {