Skip to content

Commit 6d45d10

Browse files
SkyZeroZxalan-agius4
authored andcommitted
fix(@angular/build): escape prerender redirect URLs
Escape redirect targets before embedding them in the meta refresh tag and the fallback link of generated static redirect pages. (cherry picked from commit 537a47a)
1 parent db45eed commit 6d45d10

3 files changed

Lines changed: 75 additions & 2 deletions

File tree

‎packages/angular/build/src/utils/server-rendering/utils.ts‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,53 @@ export function isSsrRequestHandler(
2222
return typeof value === 'function' && '__ng_request_handler__' in value;
2323
}
2424

25+
/**
26+
* A mapping of the characters which have to be escaped to be interpolated into HTML,
27+
* to their entity equivalents.
28+
*/
29+
const HTML_ESCAPE_CHARACTER_MAP: Record<string, string> = {
30+
'&': '&amp;',
31+
'<': '&lt;',
32+
'>': '&gt;',
33+
'"': '&quot;',
34+
"'": '&#39;',
35+
};
36+
37+
/**
38+
* Escapes the characters of a value which is interpolated into HTML text or into a quoted
39+
* attribute value.
40+
*
41+
* @param text - The value to escape.
42+
* @returns The escaped value.
43+
*/
44+
function escapeHtml(text: string): string {
45+
return text.replace(/[&<>"']/g, (character) => HTML_ESCAPE_CHARACTER_MAP[character]);
46+
}
47+
2548
/**
2649
* Generates a static HTML page with a meta refresh tag to redirect the user to a specified URL.
2750
*
2851
* This function creates a simple HTML page that performs a redirect using a meta tag.
2952
* It includes a fallback link in case the meta-refresh doesn't work.
3053
*
54+
* The provided URL is HTML-escaped before being interpolated.
55+
*
3156
* @param url - The URL to which the page should redirect.
3257
* @returns The HTML content of the static redirect page.
3358
*/
3459
export function generateRedirectStaticPage(url: string): string {
60+
const escapedUrl = escapeHtml(url);
61+
3562
return `
3663
<!DOCTYPE html>
3764
<html>
3865
<head>
3966
<meta charset="utf-8">
4067
<title>Redirecting</title>
41-
<meta http-equiv="refresh" content="0; url=${url}">
68+
<meta http-equiv="refresh" content="0; url=${escapedUrl}">
4269
</head>
4370
<body>
44-
<pre>Redirecting to <a href="${url}">${url}</a></pre>
71+
<pre>Redirecting to <a href="${escapedUrl}">${escapedUrl}</a></pre>
4572
</body>
4673
</html>
4774
`.trim();
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 { generateRedirectStaticPage } from './utils';
10+
11+
describe('generateRedirectStaticPage', () => {
12+
it('escapes the ampersands of a redirect target', () => {
13+
const page = generateRedirectStaticPage('https://example.com/docs?from=ssg&next=/ssg');
14+
15+
expect(page).toContain(
16+
'<meta http-equiv="refresh" content="0; url=https://example.com/docs?from=ssg&amp;next=/ssg">',
17+
);
18+
expect(page).toContain(
19+
'<a href="https://example.com/docs?from=ssg&amp;next=/ssg">' +
20+
'https://example.com/docs?from=ssg&amp;next=/ssg</a>',
21+
);
22+
});
23+
24+
it('escapes characters which would break out of the attribute or the tag', () => {
25+
const page = generateRedirectStaticPage(`/"><script>alert('1')</script>`);
26+
27+
expect(page).not.toContain('<script>');
28+
expect(page).toContain(
29+
'<meta http-equiv="refresh" content="0; url=' +
30+
'/&quot;&gt;&lt;script&gt;alert(&#39;1&#39;)&lt;/script&gt;">',
31+
);
32+
expect(page).toContain('<a href="/&quot;&gt;&lt;script&gt;alert(&#39;1&#39;)&lt;/script&gt;">');
33+
});
34+
});

‎tests/e2e/tests/build/server-rendering/server-routes-output-mode-static.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ export default async function () {
4848
path: 'ssg-redirect',
4949
redirectTo: 'ssg'
5050
},
51+
{
52+
path: 'ssg-redirect-external',
53+
component: Ssg,
54+
},
5155
{
5256
path: 'ssg-redirect-via-guard',
5357
canActivate: [() => {
@@ -73,6 +77,11 @@ export default async function () {
7377
import { RenderMode, ServerRoute } from '@angular/ssr';
7478
7579
export const serverRoutes: ServerRoute[] = [
80+
{
81+
path: 'ssg-redirect-external',
82+
renderMode: RenderMode.Prerender,
83+
headers: { Location: 'https://example.com/docs?from=ssg&next=/ssg' },
84+
},
7685
{
7786
path: 'ssg/:id',
7887
renderMode: RenderMode.Prerender,
@@ -115,6 +124,9 @@ export default async function () {
115124
'ssg/two/index.html': /ng-server-context="ssg".+ssg-with-params works!/,
116125
// When static redirects are generated as meta tags.
117126
'ssg-redirect/index.html': '<meta http-equiv="refresh" content="0; url=/ssg">',
127+
// The target of a 'Location' header is HTML escaped before it is written to the page.
128+
'ssg-redirect-external/index.html':
129+
'<meta http-equiv="refresh" content="0; url=https://example.com/docs?from=ssg&amp;next=/ssg">',
118130
'ssg-redirect-via-guard/index.html':
119131
'<meta http-equiv="refresh" content="0; url=/ssg?foo=bar">',
120132
};

0 commit comments

Comments
 (0)