@@ -4,6 +4,7 @@ import assert from 'node:assert';
44import {
55 expectFileNotToExist,
66 expectFileToMatch,
7+ readFile,
78 replaceInFile,
89 writeFile,
910} from '../../../utils/fs';
@@ -48,6 +49,18 @@ export default async function () {
4849 path: 'ssg-redirect',
4950 redirectTo: 'ssg'
5051 },
52+ {
53+ path: 'ssg-redirect-xss',
54+ redirectTo: '/ssg"><script>alert(1)</script>&q=x'
55+ },
56+ {
57+ path: 'ssg-redirect-external',
58+ component: Ssg,
59+ },
60+ {
61+ path: 'ssg-redirect-unsafe-url',
62+ component: Ssg,
63+ },
5164 {
5265 path: 'ssg-redirect-via-guard',
5366 canActivate: [() => {
@@ -73,6 +86,11 @@ export default async function () {
7386 import { RenderMode, ServerRoute } from '@angular/ssr';
7487
7588 export const serverRoutes: ServerRoute[] = [
89+ {
90+ path: 'ssg-redirect-external',
91+ renderMode: RenderMode.Prerender,
92+ headers: { Location: 'https://example.com/docs?from=ssg' },
93+ },
7694 {
7795 path: 'ssg/:id',
7896 renderMode: RenderMode.Prerender,
@@ -108,21 +126,81 @@ export default async function () {
108126 await replaceInFile('src/app/app.routes.server.ts', 'RenderMode.Server', 'RenderMode.Prerender');
109127 await noSilentNg('build', '--output-mode=static');
110128
111- const expects: Record<string, RegExp | string> = {
129+ const escapedXssRedirectUrl = '/ssg"><script>alert(1)</script>&q=x';
130+ const expects: Record<string, RegExp | string | (RegExp | string)[]> = {
112131 'index.html': /ng-server-context="ssg".+home works!/,
113132 'ssg/index.html': /ng-server-context="ssg".+ssg works!/,
114133 'ssg/one/index.html': /ng-server-context="ssg".+ssg-with-params works!/,
115134 'ssg/two/index.html': /ng-server-context="ssg".+ssg-with-params works!/,
116135 // When static redirects are generated as meta tags.
117136 'ssg-redirect/index.html': '<meta http-equiv="refresh" content="0; url=/ssg">',
137+ 'ssg-redirect-xss/index.html': [
138+ `<meta http-equiv="refresh" content="0; url=${escapedXssRedirectUrl}">`,
139+ `<a href="${escapedXssRedirectUrl}">${escapedXssRedirectUrl}</a>`,
140+ ],
141+ 'ssg-redirect-external/index.html': [
142+ '<meta http-equiv="refresh" content="0; url=https://example.com/docs?from=ssg">',
143+ '<a href="https://example.com/docs?from=ssg">https://example.com/docs?from=ssg</a>',
144+ ],
118145 'ssg-redirect-via-guard/index.html':
119146 '<meta http-equiv="refresh" content="0; url=/ssg?foo=bar">',
120147 };
121148
122- for (const [filePath, fileMatch] of Object.entries(expects)) {
123- await expectFileToMatch(join('dist/test-project/browser', filePath), fileMatch);
149+ for (const [filePath, fileMatches] of Object.entries(expects)) {
150+ for (const fileMatch of Array.isArray(fileMatches) ? fileMatches : [fileMatches]) {
151+ await expectFileToMatch(join('dist/test-project/browser', filePath), fileMatch);
152+ }
124153 }
125154
155+ const xssRedirectHtml = await readFile(
156+ join('dist/test-project/browser', 'ssg-redirect-xss/index.html'),
157+ );
158+ assert.doesNotMatch(xssRedirectHtml, /<script>alert\(1\)<\/script>/);
159+
160+ await replaceInFile(
161+ 'src/app/app.routes.server.ts',
162+ `{
163+ path: '**',
164+ renderMode: RenderMode.Prerender,
165+ },`,
166+ `{
167+ path: 'ssg-redirect-unsafe-url',
168+ renderMode: RenderMode.Prerender,
169+ headers: { Location: 'javascript:alert(1)' },
170+ },
171+ {
172+ path: '**',
173+ renderMode: RenderMode.Prerender,
174+ },`,
175+ );
176+
177+ const { message: unsafeProtocolErrorMessage } = await expectToFail(() =>
178+ noSilentNg('build', '--output-mode=static'),
179+ );
180+ assert.match(
181+ unsafeProtocolErrorMessage,
182+ /An error occurred while prerendering route '\/ssg-redirect-unsafe-url'/,
183+ );
184+ assert.match(unsafeProtocolErrorMessage, /Unsupported redirect URL protocol "javascript:"/);
185+
186+ await replaceInFile(
187+ 'src/app/app.routes.server.ts',
188+ `headers: { Location: 'javascript:alert(1)' },`,
189+ `headers: { Location: '/\\\\evil.com' },`,
190+ );
191+
192+ const { message: backslashRedirectErrorMessage } = await expectToFail(() =>
193+ noSilentNg('build', '--output-mode=static'),
194+ );
195+ assert.match(
196+ backslashRedirectErrorMessage,
197+ /An error occurred while prerendering route '\/ssg-redirect-unsafe-url'/,
198+ );
199+ assert.match(
200+ backslashRedirectErrorMessage,
201+ /Invalid redirect URL\. Static redirects only support HTTP\(S\) URLs and same-origin absolute paths\./,
202+ );
203+
126204 // Check that server directory does not exist
127205 assert(
128206 !existsSync('dist/test-project/server'),
0 commit comments