Skip to content

Commit b959d32

Browse files
priyanshu92Copilot
andcommitted
address PR review feedback
Harden numeric-entity icon sanitization across all five standalone report templates by decoding entity-only input to text before HTML escaping. Add regression coverage for encoded tags, benign glyphs, mixed markup, and malformed entities. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 741453d1-2d59-4081-bed7-c95fe7fe8646
1 parent e16d7e4 commit b959d32

6 files changed

Lines changed: 129 additions & 5 deletions

File tree

plugins/power-pages/agents/assets/data-model-plan.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,18 @@ <h2>ER Diagram</h2>
265265

266266
function safeIcon(value) {
267267
const icon = String(value ?? '');
268-
return /^(?:&#(?:x[0-9a-f]+|\d+);)+$/i.test(icon) ? icon : esc(icon);
268+
if (!/^(?:&#(?:x[0-9a-f]+|\d+);)+$/i.test(icon)) return esc(icon);
269+
270+
let valid = true;
271+
const decoded = icon.replace(/&#(?:x([0-9a-f]+)|(\d+));/gi, (_, hex, decimal) => {
272+
const codePoint = Number.parseInt(hex || decimal, hex ? 16 : 10);
273+
if (codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff)) {
274+
valid = false;
275+
return '';
276+
}
277+
return String.fromCodePoint(codePoint);
278+
});
279+
return valid ? esc(decoded) : esc(icon);
269280
}
270281

271282
function safeStatus(value) {

plugins/power-pages/agents/assets/permissions-plan.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,18 @@ <h3>Legend</h3>
250250

251251
function safeIcon(value) {
252252
const icon = String(value ?? '');
253-
return /^(?:&#(?:x[0-9a-f]+|\d+);)+$/i.test(icon) ? icon : esc(icon);
253+
if (!/^(?:&#(?:x[0-9a-f]+|\d+);)+$/i.test(icon)) return esc(icon);
254+
255+
let valid = true;
256+
const decoded = icon.replace(/&#(?:x([0-9a-f]+)|(\d+));/gi, (_, hex, decimal) => {
257+
const codePoint = Number.parseInt(hex || decimal, hex ? 16 : 10);
258+
if (codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff)) {
259+
valid = false;
260+
return '';
261+
}
262+
return String.fromCodePoint(codePoint);
263+
});
264+
return valid ? esc(decoded) : esc(icon);
254265
}
255266

256267
function safeScope(value) {

plugins/power-pages/scripts/tests/render-generated-plan-security.test.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ const { spawnSync } = require('node:child_process');
88

99
const scriptsDir = path.join(__dirname, '..');
1010
const attack = '<img src=x onerror="globalThis.pwned=1"> \' "</script><script>globalThis.pwned=2</script> ` ${globalThis.pwned=3}';
11+
const encodedTagAttack = [...'<img src=x onerror="globalThis.pwned=1">']
12+
.map(character => `&#${character.codePointAt(0)};`)
13+
.join('');
14+
const ICON_RATIONALE_DATA = [
15+
{ icon: encodedTagAttack, title: 'Encoded tag', desc: 'Must render as text.' },
16+
{ icon: '&#9881;', title: 'Numeric glyph', desc: 'Must render as a glyph.' },
17+
{ icon: '&#9881;<img src=x>', title: 'Mixed input', desc: 'Must remain escaped text.' },
18+
{ icon: '&#xZZ;', title: 'Malformed entity', desc: 'Must remain escaped text.' },
19+
];
1120

1221
const DATA_MODEL_DATA = {
1322
SITE_NAME: 'Contoso Portal',
@@ -259,6 +268,66 @@ test('data model plan normalizes unexpected statuses for stats, cards, and SVG c
259268
assert.ok(Object.values(attributes).every(value => value !== undefined && value !== 'undefined'));
260269
});
261270

271+
test('all icon renderers decode numeric glyphs to text before HTML escaping', () => {
272+
const reports = [
273+
{
274+
script: 'render-data-model-plan.js',
275+
data: { ...DATA_MODEL_DATA, RATIONALE_DATA: ICON_RATIONALE_DATA },
276+
options: { skipMermaid: true },
277+
},
278+
{
279+
script: 'render-permissions-plan.js',
280+
data: { ...PERMISSIONS_DATA, RATIONALE_DATA: ICON_RATIONALE_DATA },
281+
},
282+
{
283+
script: 'render-backend-plan.js',
284+
data: {
285+
SITE_NAME: 'Contoso Portal',
286+
PLAN_TITLE: 'Backend Integration Plan',
287+
SUMMARY: 'Backend plan.',
288+
ITEMS_DATA: [],
289+
RATIONALE_DATA: ICON_RATIONALE_DATA,
290+
DATA_FLOWS_DATA: [],
291+
},
292+
},
293+
{
294+
script: 'render-serverlogic-plan.js',
295+
data: {
296+
SITE_NAME: 'Contoso Portal',
297+
PLAN_TITLE: 'Server Logic Plan',
298+
SUMMARY: 'Server logic plan.',
299+
WEB_ROLES_DATA: [],
300+
SERVER_LOGICS_DATA: [],
301+
RATIONALE_DATA: ICON_RATIONALE_DATA,
302+
SECRETS_DATA: null,
303+
},
304+
},
305+
{
306+
script: 'render-cloudflow-plan.js',
307+
data: {
308+
SITE_NAME: 'Contoso Portal',
309+
PLAN_TITLE: 'Cloud Flow Plan',
310+
SUMMARY: 'Cloud flow plan.',
311+
WEB_ROLES_DATA: [],
312+
CLOUD_FLOWS_DATA: [],
313+
RATIONALE_DATA: ICON_RATIONALE_DATA,
314+
},
315+
},
316+
];
317+
318+
for (const report of reports) {
319+
const html = render(report.script, report.data);
320+
const elements = executeInlineRenderer(html, report.options);
321+
const rendered = elements.get('rationaleContainer').innerHTML;
322+
323+
assert.doesNotMatch(rendered, /<img\b/i, report.script);
324+
assert.match(rendered, /&lt;img src=x onerror=&quot;globalThis\.pwned=1&quot;&gt;/, report.script);
325+
assert.match(rendered, /<div class="principle-icon"><\/div>/, report.script);
326+
assert.match(rendered, /&amp;#9881;&lt;img src=x&gt;/, report.script);
327+
assert.match(rendered, /&amp;#xZZ;/, report.script);
328+
}
329+
});
330+
262331
test('permissions plan neutralizes hostile text, event attributes, quotes, and interpolation syntax', () => {
263332
const hostile = {
264333
...PERMISSIONS_DATA,

plugins/power-pages/skills/add-cloud-flow/assets/cloud-flow-plan.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,18 @@ <h2>Cloud Flows</h2>
143143
function esc(str) { const d = document.createElement('div'); d.textContent = str == null ? '' : String(str); return d.innerHTML; }
144144
function safeIcon(value) {
145145
const icon = String(value ?? '');
146-
return /^(?:&#(?:x[0-9a-f]+|\d+);)+$/i.test(icon) ? icon : esc(icon);
146+
if (!/^(?:&#(?:x[0-9a-f]+|\d+);)+$/i.test(icon)) return esc(icon);
147+
148+
let valid = true;
149+
const decoded = icon.replace(/&#(?:x([0-9a-f]+)|(\d+));/gi, (_, hex, decimal) => {
150+
const codePoint = Number.parseInt(hex || decimal, hex ? 16 : 10);
151+
if (codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff)) {
152+
valid = false;
153+
return '';
154+
}
155+
return String.fromCodePoint(codePoint);
156+
});
157+
return valid ? esc(decoded) : esc(icon);
147158
}
148159
function safeColor(value) { return /^#[0-9a-f]{6}$/i.test(String(value ?? '')) ? value : '#8890a4'; }
149160

plugins/power-pages/skills/add-server-logic/assets/serverlogic-plan.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,18 @@ <h2>Server Logic</h2>
174174
function esc(str) { const d = document.createElement('div'); d.textContent = str == null ? '' : String(str); return d.innerHTML; }
175175
function safeIcon(value) {
176176
const icon = String(value ?? '');
177-
return /^(?:&#(?:x[0-9a-f]+|\d+);)+$/i.test(icon) ? icon : esc(icon);
177+
if (!/^(?:&#(?:x[0-9a-f]+|\d+);)+$/i.test(icon)) return esc(icon);
178+
179+
let valid = true;
180+
const decoded = icon.replace(/&#(?:x([0-9a-f]+)|(\d+));/gi, (_, hex, decimal) => {
181+
const codePoint = Number.parseInt(hex || decimal, hex ? 16 : 10);
182+
if (codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff)) {
183+
valid = false;
184+
return '';
185+
}
186+
return String.fromCodePoint(codePoint);
187+
});
188+
return valid ? esc(decoded) : esc(icon);
178189
}
179190
function safeColor(value) { return /^#[0-9a-f]{6}$/i.test(String(value ?? '')) ? value : '#8890a4'; }
180191

plugins/power-pages/skills/integrate-backend/assets/backend-plan.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,18 @@ <h2>Integration Items</h2>
261261
function esc(str) { const d = document.createElement('div'); d.textContent = str == null ? '' : String(str); return d.innerHTML; }
262262
function safeIcon(value) {
263263
const icon = String(value ?? '');
264-
return /^(?:&#(?:x[0-9a-f]+|\d+);)+$/i.test(icon) ? icon : esc(icon);
264+
if (!/^(?:&#(?:x[0-9a-f]+|\d+);)+$/i.test(icon)) return esc(icon);
265+
266+
let valid = true;
267+
const decoded = icon.replace(/&#(?:x([0-9a-f]+)|(\d+));/gi, (_, hex, decimal) => {
268+
const codePoint = Number.parseInt(hex || decimal, hex ? 16 : 10);
269+
if (codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff)) {
270+
valid = false;
271+
return '';
272+
}
273+
return String.fromCodePoint(codePoint);
274+
});
275+
return valid ? esc(decoded) : esc(icon);
265276
}
266277
function safeUrl(value) {
267278
try {

0 commit comments

Comments
 (0)