Skip to content

Commit f2d1b66

Browse files
authored
fix(pr-deploy-site): prevent DOM based XSS (microsoft#35644)
1 parent d849b4c commit f2d1b66

File tree

2 files changed

+181
-123
lines changed

2 files changed

+181
-123
lines changed

apps/pr-deploy-site/just.config.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,22 @@ repoDeps.forEach(dep => {
5656
*/
5757
task('generate:js', () => {
5858
const jsContent = fs.readFileSync(path.join(__dirname, './pr-deploy-site.js'), 'utf-8');
59+
const placeholder = '/* __PACKAGES_LIST_PLACEHOLDER__ */';
5960

60-
if (!jsContent.includes('var packages;')) {
61-
console.error('pr-deploy-site.js must contain a line "var packages;" to replace with the actual packages');
61+
if (!jsContent.includes(placeholder)) {
62+
console.error(`pr-deploy-site.js must contain the placeholder "${placeholder}"`);
6263
process.exit(1);
6364
}
6465

6566
fs.writeFileSync(
6667
path.join('dist', 'pr-deploy-site.js'),
67-
jsContent.replace('var packages;', `var packages = ${JSON.stringify([...deployedPackages])};`),
68+
jsContent.replace(
69+
placeholder,
70+
JSON.stringify([...deployedPackages], null, 2)
71+
// remove the surrounding array brackets
72+
.slice(1, -1)
73+
.trim(),
74+
),
6875
);
6976
});
7077

Lines changed: 171 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,184 @@
11
// @ts-check
22
// If you are adding a new tile into this site, place make sure it is also being copied from `just.config.ts`
33

4-
// A build step will replace this with the list of actual built packages
5-
/** @type {string[]} */
6-
var packages;
7-
8-
var siteInfo = [
9-
{
10-
package: '@fluentui/public-docsite-resources',
11-
link: './public-docsite-resources/demo/index.html',
12-
icon: 'FavoriteStar',
13-
title: '@fluentui/react demo',
14-
},
15-
{
16-
package: '@fluentui/react',
17-
link: './react/storybook/index.html',
18-
icon: 'FavoriteStar',
19-
title: '@fluentui/react storybook',
20-
},
21-
{
22-
package: '@fluentui/public-docsite',
23-
link: './public-docsite/index.html',
24-
icon: 'Website',
25-
title: 'Website',
26-
},
27-
{
28-
package: '@fluentui/public-docsite-v9',
29-
link: './public-docsite-v9/react/index.html',
30-
icon: 'Teamwork',
31-
title: 'Converged (@fluentui/public-docsite-v9)',
32-
},
33-
{
34-
package: '@fluentui/web-components',
35-
link: './web-components/storybook/index.html',
36-
icon: 'Globe',
37-
title: 'web-components',
38-
},
39-
{
40-
package: '@fluentui/react-experiments',
41-
link: './react-experiments/demo/index.html',
42-
icon: 'TestBeaker',
43-
title: 'Experiments',
44-
},
45-
{
46-
package: '@fluentui/chart-docsite',
47-
link: './chart-docsite/storybook/index.html',
48-
icon: 'BarChart4',
49-
title: 'Charts v9',
50-
},
51-
{
52-
package: '@fluentui/react-charting',
53-
link: './react-charting/demo/index.html',
54-
icon: 'BarChart4',
55-
title: 'Charting',
56-
},
57-
{
58-
package: '@fluentui/chart-web-components',
59-
link: './chart-web-components/storybook/index.html',
60-
icon: 'BarChart4',
61-
title: 'Chart web components',
62-
},
63-
{
64-
package: '@fluentui/theming-designer',
65-
link: './theming-designer/index.html',
66-
icon: 'CheckMark',
67-
title: 'Theme Designer Example',
68-
},
69-
{
70-
package: '@fluentui/theme-designer',
71-
link: './theme-designer/storybook/index.html',
72-
icon: 'CheckMark',
73-
title: 'Theme Designer v9',
74-
},
75-
{
76-
package: '@fluentui/perf-test',
77-
link: './perf-test/index.html',
78-
icon: 'SpeedHigh',
79-
title: 'Perf Tests',
80-
},
81-
{
82-
package: '@fluentui/perf-test-react-components',
83-
link: './perf-test-react-components/index.html',
84-
icon: 'SpeedHigh',
85-
title: 'Perf Tests React-Components',
86-
},
87-
];
88-
89-
// location.pathname will be like /pull/17568/ or /heads/master/
90-
var hrefMatch = window.location.pathname.match(/^\/(pull|heads)\/([^/]+)/);
91-
var repoUrl = 'https://github.com/microsoft/fluentui';
92-
if (hrefMatch) {
93-
var link = /** @type {HTMLAnchorElement} */ (document.getElementById('prLink'));
94-
if (hrefMatch[1] === 'heads') {
95-
// master or other branch CI
96-
// eslint-disable-next-line @microsoft/sdl/no-inner-html -- Only used during PR publish, not production code.
97-
link.innerHTML = hrefMatch[2];
98-
link.href = repoUrl + '/tree/' + hrefMatch[2];
4+
main();
5+
6+
/** @typedef {{package: string; link: string; icon: string; title: string}} SiteInfo */
7+
8+
function main() {
9+
/**
10+
* NOTE: A build step will replace this with the list of actual built packages
11+
* @type {string[]}
12+
*/
13+
var packages = [
14+
/* __PACKAGES_LIST_PLACEHOLDER__ */
15+
];
16+
17+
/**
18+
* @type {SiteInfo[]}
19+
*/
20+
var siteInfo = [
21+
{
22+
package: '@fluentui/public-docsite-resources',
23+
link: './public-docsite-resources/demo/index.html',
24+
icon: 'FavoriteStar',
25+
title: '@fluentui/react demo',
26+
},
27+
{
28+
package: '@fluentui/react',
29+
link: './react/storybook/index.html',
30+
icon: 'FavoriteStar',
31+
title: '@fluentui/react storybook',
32+
},
33+
{
34+
package: '@fluentui/public-docsite',
35+
link: './public-docsite/index.html',
36+
icon: 'Website',
37+
title: 'Website',
38+
},
39+
{
40+
package: '@fluentui/public-docsite-v9',
41+
link: './public-docsite-v9/react/index.html',
42+
icon: 'Teamwork',
43+
title: 'Converged (@fluentui/public-docsite-v9)',
44+
},
45+
{
46+
package: '@fluentui/web-components',
47+
link: './web-components/storybook/index.html',
48+
icon: 'Globe',
49+
title: 'web-components',
50+
},
51+
{
52+
package: '@fluentui/react-experiments',
53+
link: './react-experiments/demo/index.html',
54+
icon: 'TestBeaker',
55+
title: 'Experiments',
56+
},
57+
{
58+
package: '@fluentui/chart-docsite',
59+
link: './chart-docsite/storybook/index.html',
60+
icon: 'BarChart4',
61+
title: 'Charts v9',
62+
},
63+
{
64+
package: '@fluentui/react-charting',
65+
link: './react-charting/demo/index.html',
66+
icon: 'BarChart4',
67+
title: 'Charting',
68+
},
69+
{
70+
package: '@fluentui/chart-web-components',
71+
link: './chart-web-components/storybook/index.html',
72+
icon: 'BarChart4',
73+
title: 'Chart web components',
74+
},
75+
{
76+
package: '@fluentui/theming-designer',
77+
link: './theming-designer/index.html',
78+
icon: 'CheckMark',
79+
title: 'Theme Designer Example',
80+
},
81+
{
82+
package: '@fluentui/theme-designer',
83+
link: './theme-designer/storybook/index.html',
84+
icon: 'CheckMark',
85+
title: 'Theme Designer v9',
86+
},
87+
{
88+
package: '@fluentui/perf-test',
89+
link: './perf-test/index.html',
90+
icon: 'SpeedHigh',
91+
title: 'Perf Tests',
92+
},
93+
{
94+
package: '@fluentui/perf-test-react-components',
95+
link: './perf-test-react-components/index.html',
96+
icon: 'SpeedHigh',
97+
title: 'Perf Tests React-Components',
98+
},
99+
];
100+
101+
updatePrOrBranchLink(window.location.pathname);
102+
renderSiteLinks(packages, siteInfo);
103+
}
104+
105+
/**
106+
* Updates the PR/branch link based on the current path.
107+
*
108+
* Note: Do not use `innerHTML` here. `window.location.pathname` is user-controlled,
109+
* and branch names can contain characters which would lead to XSS if injected as HTML.
110+
* @param {string} urlPath
111+
*/
112+
function updatePrOrBranchLink(urlPath) {
113+
// location.pathname will be like /pull/17568/ or /heads/master/
114+
var hrefMatch = urlPath.match(/^\/(pull|heads)\/([^/]+)/);
115+
if (!hrefMatch) {
116+
return;
117+
}
118+
119+
var link = /** @type {HTMLAnchorElement | null} */ (document.getElementById('prLink'));
120+
if (!link) {
121+
return;
122+
}
123+
124+
var repoUrl = 'https://github.com/microsoft/fluentui';
125+
var type = hrefMatch[1];
126+
var value = hrefMatch[2];
127+
128+
if (type === 'heads') {
129+
// NOTE: this isn't used anymore, we deploy only from PRs, but keeping the code for potential future use
130+
link.textContent = value;
131+
link.href = repoUrl + '/tree/' + encodeURIComponent(value);
132+
99133
// remove the PR-specific explanation
100134
var prExplanation = document.getElementById('prExplanation');
101135
if (prExplanation && prExplanation.parentElement) {
102136
prExplanation.parentElement.removeChild(prExplanation);
103137
}
104138
} else {
105-
// PR
106-
// eslint-disable-next-line @microsoft/sdl/no-inner-html -- Only used during PR publish, not production code.
107-
link.innerHTML = 'PR #' + hrefMatch[2];
108-
link.href = repoUrl + '/pull/' + hrefMatch[2];
139+
// PR numbers should be digits; bail if not.
140+
if (!/^\d+$/.test(value)) {
141+
return;
142+
}
143+
144+
link.textContent = 'PR #' + value;
145+
link.href = repoUrl + '/pull/' + value;
109146
}
110147
}
111148

112-
var siteLink = document.getElementById('site-list');
113-
114-
siteInfo.forEach(function (info) {
115-
if (packages.indexOf(info.package) > -1) {
116-
var li = document.createElement('LI');
117-
li.className = 'Tile';
118-
// eslint-disable-next-line @microsoft/sdl/no-inner-html -- Only used during PR publish, not production code.
119-
li.innerHTML =
120-
'<a href="' +
121-
info.link +
122-
'" class="Tile-link">' +
123-
'<i class="ms-Icon ms-Icon--' +
124-
info.icon +
125-
'"></i>' +
126-
info.title +
127-
'</a>';
128-
129-
if (siteLink) {
130-
siteLink.appendChild(li);
131-
}
149+
/**
150+
*
151+
* @param {string[]} packages
152+
* @param {SiteInfo[]} siteInfo
153+
* @returns
154+
*/
155+
function renderSiteLinks(packages, siteInfo) {
156+
if (!packages || packages.length === 0) {
157+
return;
132158
}
133-
});
159+
160+
var siteLink = document.getElementById('site-list');
161+
if (!siteLink) {
162+
return;
163+
}
164+
165+
siteInfo.forEach(function (info) {
166+
if (packages.indexOf(info.package) > -1) {
167+
var li = /** @type {HTMLLIElement} */ (document.createElement('LI'));
168+
li.className = 'Tile';
169+
170+
var a = /** @type {HTMLAnchorElement} */ (document.createElement('A'));
171+
a.href = info.link;
172+
a.className = 'Tile-link';
173+
174+
var icon = document.createElement('I');
175+
icon.className = 'ms-Icon ms-Icon--' + info.icon;
176+
a.appendChild(icon);
177+
a.appendChild(document.createTextNode(info.title));
178+
179+
li.appendChild(a);
180+
181+
/** @type {HTMLUListElement} */ (siteLink).appendChild(li);
182+
}
183+
});
184+
}

0 commit comments

Comments
 (0)