Skip to content

Commit e73bed3

Browse files
authored
HtmlEncode the JSON data before embedding. (#7238)
1 parent 7f749a1 commit e73bed3

3 files changed

Lines changed: 23 additions & 18 deletions

File tree

src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Formats/Html/HtmlReportWriter.cs

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Linq;
8+
using System.Net;
89
using System.Text;
910
using System.Text.Json;
1011
using System.Threading;
@@ -34,6 +35,11 @@ public async ValueTask WriteReportAsync(
3435
createdAt: DateTime.UtcNow,
3536
generatorVersion: Constants.Version);
3637

38+
// Serialize the dataset to JSON, then HTML-encode it for safe embedding in a data attribute.
39+
// This pattern avoids XSS vulnerabilities that can occur when embedding JSON directly in script blocks.
40+
string json = JsonSerializer.Serialize(dataset, JsonUtilities.Compact.DatasetTypeInfo);
41+
string htmlEncodedJson = WebUtility.HtmlEncode(json);
42+
3743
using var stream =
3844
new FileStream(
3945
reportFilePath,
@@ -47,22 +53,12 @@ public async ValueTask WriteReportAsync(
4753

4854
#if NET
4955
await writer.WriteAsync(HtmlTemplateBefore.AsMemory(), cancellationToken).ConfigureAwait(false);
50-
await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
51-
#else
52-
await writer.WriteAsync(HtmlTemplateBefore).ConfigureAwait(false);
53-
await writer.FlushAsync().ConfigureAwait(false);
54-
#endif
55-
56-
await JsonSerializer.SerializeAsync(
57-
stream,
58-
dataset,
59-
JsonUtilities.Compact.DatasetTypeInfo,
60-
cancellationToken).ConfigureAwait(false);
61-
62-
#if NET
56+
await writer.WriteAsync(htmlEncodedJson.AsMemory(), cancellationToken).ConfigureAwait(false);
6357
await writer.WriteAsync(HtmlTemplateAfter.AsMemory(), cancellationToken).ConfigureAwait(false);
6458
await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
6559
#else
60+
await writer.WriteAsync(HtmlTemplateBefore).ConfigureAwait(false);
61+
await writer.WriteAsync(htmlEncodedJson).ConfigureAwait(false);
6662
await writer.WriteAsync(HtmlTemplateAfter).ConfigureAwait(false);
6763
await writer.FlushAsync().ConfigureAwait(false);
6864
#endif
@@ -86,8 +82,8 @@ static HtmlReportWriter()
8682
using var reader = new StreamReader(resourceStream);
8783
string all = reader.ReadToEnd();
8884

89-
// This is the placeholder for the results array in the template.
90-
const string SearchString = @"{scenarioRunResults:[]}";
85+
// This is the placeholder in the data-dataset attribute on the root div.
86+
const string SearchString = "##DATASETS##";
9187

9288
int start = all.IndexOf(SearchString, StringComparison.Ordinal);
9389
if (start == -1)

src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/html-report/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
</head>
1515

1616
<body>
17-
<div id="root"></div>
17+
<div id="root" data-dataset="##DATASETS##"></div>
1818
<script type="module" src="./src/main.tsx"></script>
1919
</body>
2020

src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/html-report/src/main.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,25 @@ import { ReportContextProvider } from '../../components/ReportContext.tsx';
1010

1111
let dataset: Dataset = { scenarioRunResults: [] };
1212

13+
const rootElement = document.getElementById('root')!;
14+
1315
if (!import.meta.env.PROD) {
14-
// This only runs in development. In production the data is embedded into the dataset variable declaration above.
16+
// This only runs in development. In production the data is read from the data-dataset attribute on the root element.
1517
// run `node init-devdata.js` to populate the data file from the most recent execution.
1618
const imported = await import("../devdata.json");
1719
dataset = imported.default as unknown as Dataset;
20+
} else {
21+
// In production, the data is HTML-encoded and placed in a data-dataset attribute.
22+
// This pattern avoids XSS vulnerabilities that can occur when embedding JSON in script blocks.
23+
const datasetJson = rootElement.getAttribute('data-dataset');
24+
if (datasetJson) {
25+
dataset = JSON.parse(datasetJson) as Dataset;
26+
}
1827
}
1928

2029
const scoreSummary = createScoreSummary(dataset);
2130

22-
createRoot(document.getElementById('root')!).render(
31+
createRoot(rootElement).render(
2332
<FluentProvider theme={webLightTheme}>
2433
<StrictMode>
2534
<ReportContextProvider dataset={dataset} scoreSummary={scoreSummary}>

0 commit comments

Comments
 (0)