Skip to content

Commit dc1238e

Browse files
committed
fix(@schematics/angular): add trusted-proxy-headers migration
Adds a new schematic migration to automatically add trustProxyHeaders configuration to server.ts files in workspaces where either AngularNodeAppEngine or AngularAppEngine are used.
1 parent b287640 commit dc1238e

3 files changed

Lines changed: 204 additions & 1 deletion

File tree

packages/schematics/angular/migrations/migration-collection.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"add-istanbul-instrumenter": {
55
"version": "22.0.0",
66
"factory": "./add-istanbul-instrumenter/migration",
7-
"description": "Add istanbul-lib-instrument to devDependencies if Karma unit testing is used."
7+
"description": "Add 'istanbul-lib-instrument' to 'devDependencies' if Karma unit testing is used."
88
},
99
"use-application-builder": {
1010
"version": "22.0.0",
@@ -20,6 +20,11 @@
2020
"description": "Migrate projects using legacy Karma unit-test builder to the new unit-test builder with Vitest.",
2121
"optional": true
2222
},
23+
"trust-proxy-headers": {
24+
"version": "22.0.0",
25+
"factory": "./trust-proxy-headers/migration",
26+
"description": "Add 'trustProxyHeaders' configuration to 'AngularNodeAppEngine' or 'AngularAppEngine'. For more information see: https://angular.dev/best-practices/security#configuring-trusted-proxy-headers"
27+
},
2328
"update-workspace-config": {
2429
"version": "22.0.0",
2530
"factory": "./update-workspace-config/migration",
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { Rule } from '@angular-devkit/schematics';
10+
import ts from 'typescript';
11+
import { allTargetOptions, allWorkspaceTargets, getWorkspace } from '../../utility/workspace';
12+
13+
const TODO_COMMENT =
14+
'// TODO: This is a security-sensitive option. Remove if not needed. ' +
15+
'For more information, see https://angular.dev/best-practices/security#configuring-trusted-proxy-headers';
16+
17+
export default function (): Rule {
18+
return async (tree) => {
19+
const workspace = await getWorkspace(tree);
20+
const serverFiles = new Set<string>();
21+
22+
for (const [targetName, target] of allWorkspaceTargets(workspace)) {
23+
if (targetName !== 'build') {
24+
continue;
25+
}
26+
27+
for (const [, options] of allTargetOptions(target)) {
28+
if (typeof options?.['server'] === 'string') {
29+
serverFiles.add(options['server']);
30+
}
31+
}
32+
}
33+
34+
for (const path of serverFiles) {
35+
if (!tree.exists(path)) {
36+
continue;
37+
}
38+
39+
const content = tree.readText(path);
40+
if (content.includes(TODO_COMMENT)) {
41+
continue;
42+
}
43+
44+
if (!content.includes('AngularAppEngine') && !content.includes('AngularNodeAppEngine')) {
45+
continue;
46+
}
47+
48+
const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
49+
const recorder = tree.beginUpdate(path);
50+
51+
function visit(node: ts.Node) {
52+
if (
53+
ts.isNewExpression(node) &&
54+
ts.isIdentifier(node.expression) &&
55+
(node.expression.text === 'AngularNodeAppEngine' ||
56+
node.expression.text === 'AngularAppEngine')
57+
) {
58+
// Check arguments
59+
if (!node.arguments || node.arguments.length === 0) {
60+
// Case 1: No arguments passed
61+
const insertPos = node.end - 1; // right before )
62+
recorder.insertRight(
63+
insertPos,
64+
`{\n ${TODO_COMMENT}\n ` +
65+
`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],\n}`,
66+
);
67+
} else if (node.arguments.length > 0) {
68+
const firstArg = node.arguments[0];
69+
if (ts.isObjectLiteralExpression(firstArg)) {
70+
// Check if trustProxyHeaders is already present
71+
const hasTrustProxyHeaders = firstArg.properties.some(
72+
(prop: ts.ObjectLiteralElementLike) =>
73+
ts.isPropertyAssignment(prop) &&
74+
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
75+
prop.name.text === 'trustProxyHeaders',
76+
);
77+
78+
if (!hasTrustProxyHeaders) {
79+
// Insert right after the opening brace
80+
const insertPos = firstArg.getStart() + 1;
81+
recorder.insertRight(
82+
insertPos,
83+
`\n ${TODO_COMMENT}\n ` +
84+
`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`,
85+
);
86+
}
87+
}
88+
}
89+
}
90+
ts.forEachChild(node, visit);
91+
}
92+
93+
visit(sourceFile);
94+
tree.commitUpdate(recorder);
95+
}
96+
};
97+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { EmptyTree } from '@angular-devkit/schematics';
10+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
11+
12+
function createWorkSpaceConfig(tree: UnitTestTree) {
13+
const angularConfig = {
14+
version: 1,
15+
projects: {
16+
app: {
17+
root: '',
18+
sourceRoot: 'src',
19+
projectType: 'application',
20+
architect: {
21+
build: {
22+
options: {
23+
server: '/server.ts',
24+
},
25+
},
26+
},
27+
},
28+
},
29+
};
30+
31+
tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2));
32+
}
33+
34+
describe(`Migration to add trustProxyHeaders to server.ts`, () => {
35+
const schematicName = 'trust-proxy-headers';
36+
const schematicRunner = new SchematicTestRunner(
37+
'migrations',
38+
require.resolve('../migration-collection.json'),
39+
);
40+
const TODO_COMMENT =
41+
'// TODO: This is a security-sensitive option. Remove if not needed. ' +
42+
'For more information, see https://angular.dev/best-practices/security#configuring-trusted-proxy-headers';
43+
44+
let tree: UnitTestTree;
45+
beforeEach(() => {
46+
tree = new UnitTestTree(new EmptyTree());
47+
createWorkSpaceConfig(tree);
48+
});
49+
50+
it(`should add trustProxyHeaders to AngularNodeAppEngine with no args`, async () => {
51+
tree.create(
52+
'/server.ts',
53+
`import { AngularNodeAppEngine } from '@angular/ssr/node';\nconst angularApp = new AngularNodeAppEngine();`,
54+
);
55+
56+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
57+
const content = newTree.readText('/server.ts');
58+
expect(content).toContain(`const angularApp = new AngularNodeAppEngine({`);
59+
expect(content).toContain(TODO_COMMENT);
60+
expect(content).toContain(`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`);
61+
});
62+
63+
it(`should add trustProxyHeaders to AngularNodeAppEngine with existing args`, async () => {
64+
tree.create(
65+
'/server.ts',
66+
`import { AngularNodeAppEngine } from '@angular/ssr/node';\n` +
67+
`const angularApp = new AngularNodeAppEngine({\n allowedHosts: ['localhost']\n});`,
68+
);
69+
70+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
71+
const content = newTree.readText('/server.ts');
72+
expect(content).toContain(`const angularApp = new AngularNodeAppEngine({`);
73+
expect(content).toContain(TODO_COMMENT);
74+
expect(content).toContain(`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`);
75+
expect(content).toContain(`allowedHosts: ['localhost']`);
76+
});
77+
78+
it(`should add trustProxyHeaders to AngularAppEngine`, async () => {
79+
tree.create(
80+
'/server.ts',
81+
`import { AngularAppEngine } from '@angular/ssr';\nconst angularApp = new AngularAppEngine();`,
82+
);
83+
84+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
85+
const content = newTree.readText('/server.ts');
86+
expect(content).toContain(`const angularApp = new AngularAppEngine({`);
87+
expect(content).toContain(TODO_COMMENT);
88+
expect(content).toContain(`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`);
89+
});
90+
91+
it(`should not add trustProxyHeaders if it already exists`, async () => {
92+
const originalContent =
93+
`import { AngularAppEngine } from '@angular/ssr';\n` +
94+
`const angularApp = new AngularAppEngine({\n trustProxyHeaders: true\n});`;
95+
tree.create('/server.ts', originalContent);
96+
97+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
98+
const content = newTree.readText('/server.ts');
99+
expect(content).toBe(originalContent);
100+
});
101+
});

0 commit comments

Comments
 (0)