@@ -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' : / n g - s e r v e r - c o n t e x t = " s s g " .+ h o m e w o r k s ! / ,
113132 'ssg/index.html' : / n g - s e r v e r - c o n t e x t = " s s g " .+ s s g w o r k s ! / ,
114133 'ssg/one/index.html' : / n g - s e r v e r - c o n t e x t = " s s g " .+ s s g - w i t h - p a r a m s w o r k s ! / ,
115134 'ssg/two/index.html' : / n g - s e r v e r - c o n t e x t = " s s g " .+ s s g - w i t h - p a r a m s w o r k s ! / ,
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 , / < s c r i p t > a l e r t \( 1 \) < \/ s c r i p t > / ) ;
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+ / A n e r r o r o c c u r r e d w h i l e p r e r e n d e r i n g r o u t e ' \/ s s g - r e d i r e c t - u n s a f e - u r l ' / ,
183+ ) ;
184+ assert . match ( unsafeProtocolErrorMessage , / U n s u p p o r t e d r e d i r e c t U R L p r o t o c o l " j a v a s c r i p t : " / ) ;
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+ / A n e r r o r o c c u r r e d w h i l e p r e r e n d e r i n g r o u t e ' \/ s s g - r e d i r e c t - u n s a f e - u r l ' / ,
198+ ) ;
199+ assert . match (
200+ backslashRedirectErrorMessage ,
201+ / I n v a l i d r e d i r e c t U R L \. S t a t i c r e d i r e c t s o n l y s u p p o r t H T T P \( S \) U R L s a n d s a m e - o r i g i n a b s o l u t e p a t h s \. / ,
202+ ) ;
203+
126204 // Check that server directory does not exist
127205 assert (
128206 ! existsSync ( 'dist/test-project/server' ) ,
0 commit comments