Skip to content

Commit e508095

Browse files
Address security review bounds checks
1 parent f82178c commit e508095

6 files changed

Lines changed: 188 additions & 9 deletions

File tree

OfficeIMO.Html/Rtf/Internal/RtfHtmlReader.Fields.cs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,98 @@ private bool IsHyperlinkFieldAllowed(IElement token, string? instruction) {
5959
return AreHyperlinkFieldTargetsAllowed(token, null);
6060
}
6161

62+
if (!TryReadHyperlinkInstructionTargets(instruction!, out IReadOnlyList<string> instructionTargets)) {
63+
return false;
64+
}
65+
6266
var field = new RtfField(instruction!);
63-
return AreHyperlinkFieldTargetsAllowed(token, field.HyperlinkField?.Target?.ToString());
67+
string? instructionTarget = instructionTargets.Count == 0
68+
? field.HyperlinkField?.Target?.ToString()
69+
: instructionTargets[0];
70+
return AreHyperlinkFieldTargetsAllowed(token, instructionTarget);
71+
}
72+
73+
private bool TryReadHyperlinkInstructionTargets(string instruction, out IReadOnlyList<string> targets) {
74+
targets = Array.Empty<string>();
75+
IReadOnlyList<string> tokens = TokenizeRtfFieldInstruction(instruction);
76+
if (tokens.Count == 0 || !string.Equals(tokens[0], "HYPERLINK", StringComparison.OrdinalIgnoreCase)) {
77+
return true;
78+
}
79+
80+
var values = new List<string>();
81+
for (int index = 1; index < tokens.Count; index++) {
82+
string token = tokens[index];
83+
if (token.Length == 0) {
84+
continue;
85+
}
86+
87+
if (token[0] == '\\') {
88+
if (RtfHyperlinkSwitchConsumesValue(token) && index + 1 < tokens.Count) {
89+
index++;
90+
}
91+
92+
continue;
93+
}
94+
95+
values.Add(token);
96+
}
97+
98+
if (values.Count > 1) {
99+
_options.AddDiagnostic(
100+
"RtfHtmlFieldHyperlinkRejected",
101+
"RTF hyperlink field instruction contains multiple targets.",
102+
"data-officeimo-rtf-field-instruction");
103+
return false;
104+
}
105+
106+
targets = values;
107+
return true;
108+
}
109+
110+
private static bool RtfHyperlinkSwitchConsumesValue(string token) =>
111+
string.Equals(token, "\\l", StringComparison.OrdinalIgnoreCase) ||
112+
string.Equals(token, "\\m", StringComparison.OrdinalIgnoreCase) ||
113+
string.Equals(token, "\\n", StringComparison.OrdinalIgnoreCase) ||
114+
string.Equals(token, "\\o", StringComparison.OrdinalIgnoreCase) ||
115+
string.Equals(token, "\\t", StringComparison.OrdinalIgnoreCase);
116+
117+
private static IReadOnlyList<string> TokenizeRtfFieldInstruction(string instruction) {
118+
var tokens = new List<string>();
119+
int index = 0;
120+
while (index < instruction.Length) {
121+
while (index < instruction.Length && char.IsWhiteSpace(instruction[index])) {
122+
index++;
123+
}
124+
125+
if (index >= instruction.Length) {
126+
break;
127+
}
128+
129+
if (instruction[index] == '"') {
130+
index++;
131+
var quoted = new System.Text.StringBuilder();
132+
while (index < instruction.Length) {
133+
char c = instruction[index++];
134+
if (c == '"') {
135+
break;
136+
}
137+
138+
quoted.Append(c);
139+
}
140+
141+
tokens.Add(quoted.ToString());
142+
continue;
143+
}
144+
145+
int start = index;
146+
while (index < instruction.Length && !char.IsWhiteSpace(instruction[index])) {
147+
index++;
148+
}
149+
150+
tokens.Add(instruction.Substring(start, index - start));
151+
}
152+
153+
return tokens;
64154
}
65155

66156
private bool AreHyperlinkFieldTargetsAllowed(IElement token, string? instructionTarget) {

OfficeIMO.Pdf/Reading/Font/ToUnicodeCMap.cs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ internal sealed class ToUnicodeCMap {
1010
private readonly Dictionary<string, string> _reverseMap = new(StringComparer.Ordinal);
1111
private int _maxKeyBytes = 1;
1212
private int _maxReverseTextLength = 1;
13+
private int _processedMappings;
1314

1415
public static bool TryParse(byte[] data, out ToUnicodeCMap? cmap) {
1516
try {
@@ -25,20 +26,36 @@ private void Parse(string s) {
2526
var bfchar = new Regex(@"<(?<src>[0-9A-Fa-f]+)>\s+<(?<dst>[0-9A-Fa-f]+)>");
2627
foreach (Match section in Regex.Matches(s, @"beginbfchar([\s\S]*?)endbfchar", RegexOptions.IgnoreCase)) {
2728
foreach (Match m in bfchar.Matches(section.Groups[1].Value)) {
29+
if (HasReachedMappingLimit()) {
30+
break;
31+
}
32+
2833
AddMap(m.Groups["src"].Value, m.Groups["dst"].Value);
2934
}
35+
36+
if (HasReachedMappingLimit()) {
37+
break;
38+
}
3039
}
3140
// Handle beginbfrange / endbfrange, sequential mapping
3241
var bfrangeLine = new Regex(@"<(?<from>[0-9A-Fa-f]+)>\s+<(?<to>[0-9A-Fa-f]+)>\s+<(?<dst>[0-9A-Fa-f]+)>");
3342
foreach (Match section in Regex.Matches(s, @"beginbfrange([\s\S]*?)endbfrange", RegexOptions.IgnoreCase)) {
43+
if (HasReachedMappingLimit()) {
44+
break;
45+
}
46+
3447
string body = section.Groups[1].Value;
3548
string sequentialBody = Regex.Replace(body, @"<(?<from>[0-9A-Fa-f]+)>\s+<(?<to>[0-9A-Fa-f]+)>\s+\[(?<dsts>[\s\S]*?)\]", string.Empty, RegexOptions.IgnoreCase);
3649
foreach (Match m in bfrangeLine.Matches(sequentialBody)) {
50+
if (HasReachedMappingLimit()) {
51+
break;
52+
}
53+
3754
int from = Convert.ToInt32(m.Groups["from"].Value, 16);
3855
int to = Convert.ToInt32(m.Groups["to"].Value, 16);
3956
int dst = Convert.ToInt32(m.Groups["dst"].Value, 16);
4057
int rangeLength = to >= from ? to - from + 1 : 0;
41-
if (rangeLength <= 0 || rangeLength > MaxRangeMappings || _map.Count + rangeLength > MaxMappings) {
58+
if (rangeLength <= 0 || rangeLength > MaxRangeMappings || _processedMappings + rangeLength > MaxMappings) {
4259
continue;
4360
}
4461

@@ -51,6 +68,10 @@ private void Parse(string s) {
5168
}
5269

5370
foreach (Match m in Regex.Matches(body, @"<(?<from>[0-9A-Fa-f]+)>\s+<(?<to>[0-9A-Fa-f]+)>\s+\[(?<dsts>[\s\S]*?)\]", RegexOptions.IgnoreCase)) {
71+
if (HasReachedMappingLimit()) {
72+
break;
73+
}
74+
5475
int from = Convert.ToInt32(m.Groups["from"].Value, 16);
5576
int to = Convert.ToInt32(m.Groups["to"].Value, 16);
5677
int keyBytes = m.Groups["from"].Value.Length / 2;
@@ -60,7 +81,7 @@ private void Parse(string s) {
6081
break;
6182
}
6283

63-
if (_map.Count >= MaxMappings || code - from >= MaxRangeMappings) {
84+
if (HasReachedMappingLimit() || code - from >= MaxRangeMappings) {
6485
break;
6586
}
6687

@@ -73,10 +94,11 @@ private void Parse(string s) {
7394
}
7495

7596
private void AddMap(string srcHex, string dstHex) {
76-
if (_map.Count >= MaxMappings) {
97+
if (HasReachedMappingLimit()) {
7798
return;
7899
}
79100

101+
_processedMappings++;
80102
srcHex = RemoveHexWhitespace(srcHex);
81103
dstHex = RemoveHexWhitespace(dstHex);
82104
if (srcHex.Length % 2 != 0) srcHex = "0" + srcHex;
@@ -94,6 +116,8 @@ private void AddMap(string srcHex, string dstHex) {
94116
}
95117
}
96118

119+
private bool HasReachedMappingLimit() => _processedMappings >= MaxMappings;
120+
97121
private static string RemoveHexWhitespace(string value) {
98122
bool hasWhitespace = false;
99123
for (int i = 0; i < value.Length; i++) {

OfficeIMO.Tests/Pdf/PdfDocumentChartDrawingTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,23 @@ public void WordChartSeriesExtraction_ExtendsCategoriesAcrossAllSeries() {
178178
Assert.Equal(new[] { "Q1", "Q2", "Q3", "Q4" }, categories);
179179
}
180180

181+
[Fact]
182+
public void WordChartSeriesExtraction_RejectsTooManySeriesBeforeOrdering() {
183+
var chart = new BarChart(
184+
new BarDirection { Val = BarDirectionValues.Column },
185+
new BarGrouping { Val = BarGroupingValues.Clustered });
186+
for (uint index = 0; index < 257; index++) {
187+
chart.Append(CreateBarSeries(index, new[] { "Q1" }, new[] { 1D }));
188+
}
189+
190+
MethodInfo method = typeof(WordPdfConverterExtensions).GetMethod("ExtractNativeWordChartSeries", BindingFlags.NonPublic | BindingFlags.Static)!;
191+
object?[] args = { new Chart(), chart, OfficeChartKind.ColumnClustered, new Dictionary<A.SchemeColorValues, OfficeColor>(), null };
192+
193+
TargetInvocationException exception = Assert.Throws<TargetInvocationException>(() => method.Invoke(null, args));
194+
195+
Assert.Contains("maximum supported series count", exception.InnerException?.Message);
196+
}
197+
181198
[Fact]
182199
public void WordChartSeriesExtraction_PreservesNonPiePointFillColors() {
183200
OfficeColor highlight = OfficeColor.ParseHex("#F76707");

OfficeIMO.Tests/Pdf/PdfFontSecurityTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Reflection;
34
using System.Text;
45
using OfficeIMO.Pdf;
56
using Xunit;
@@ -25,6 +26,24 @@ public void ToUnicodeCMap_SkipsOversizedSequentialRanges() {
2526
Assert.NotEqual("B", cmap.MapBytes(new byte[] { 0x10, 0x00 }));
2627
}
2728

29+
[Fact]
30+
public void ToUnicodeCMap_CapsDuplicateSourceEntriesByProcessedCount() {
31+
var builder = new StringBuilder();
32+
builder.AppendLine("beginbfchar");
33+
for (int index = 0; index < 70000; index++) {
34+
builder.AppendLine("<01> <0041>");
35+
}
36+
37+
builder.AppendLine("endbfchar");
38+
39+
Assert.True(ToUnicodeCMap.TryParse(Encoding.ASCII.GetBytes(builder.ToString()), out ToUnicodeCMap? cmap));
40+
Assert.NotNull(cmap);
41+
42+
FieldInfo field = typeof(ToUnicodeCMap).GetField("_processedMappings", BindingFlags.NonPublic | BindingFlags.Instance)!;
43+
Assert.Equal(65536, (int)field.GetValue(cmap!)!);
44+
Assert.Equal("A", cmap!.MapBytes(new byte[] { 0x01 }));
45+
}
46+
2847
[Fact]
2948
public void ResourceResolver_CapsCidWidthRangeExpansion() {
3049
var page = new PdfDictionary();

OfficeIMO.Tests/Rtf/RtfHtmlFieldTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,25 @@ public void Html_ToRtfDocument_Rejects_Unsafe_Hyperlink_Field_Instruction_Target
105105
Assert.Equal("data-officeimo-rtf-field-instruction", diagnostic.Source);
106106
}
107107

108+
[Fact]
109+
public void Html_ToRtfDocument_Rejects_Hyperlink_Field_Instruction_With_Extra_Target() {
110+
const string html = "<p><a href=\"https://example.test/\" data-officeimo-rtf-field=\"true\" data-officeimo-rtf-field-instruction=\"HYPERLINK &quot;file:///C:/secret.txt&quot; &quot;https://example.test/&quot;\">Link</a></p>";
111+
var options = new HtmlToRtfOptions {
112+
UrlPolicy = HtmlUrlPolicy.CreateWebOnlyProfile()
113+
};
114+
115+
RtfDocument document = html.ToRtfDocument(options);
116+
RtfParagraph paragraph = Assert.Single(document.Paragraphs);
117+
string rtf = document.ToRtf();
118+
119+
Assert.Empty(paragraph.Inlines.OfType<RtfField>());
120+
Assert.Equal("Link", paragraph.ToPlainText());
121+
Assert.DoesNotContain("file:///C:/secret.txt", rtf, StringComparison.OrdinalIgnoreCase);
122+
HtmlRtfConversionDiagnostic diagnostic = Assert.Single(options.Diagnostics);
123+
Assert.Equal("RtfHtmlFieldHyperlinkRejected", diagnostic.Code);
124+
Assert.Equal("data-officeimo-rtf-field-instruction", diagnostic.Source);
125+
}
126+
108127
[Fact]
109128
public void Html_ToRtfDocument_Rejects_Unsafe_Hyperlink_Field_Metadata_Target() {
110129
const string html = "<p><a href=\"https://example.test/\" data-officeimo-rtf-field=\"true\" data-officeimo-rtf-field-instruction=\"HYPERLINK &quot;https://example.test/&quot;\" data-officeimo-rtf-field-hyperlink=\"javascript:alert(1)\">Link</a></p>";

OfficeIMO.Word.Pdf/WordPdfConverterExtensions.Native.Charts.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -439,13 +439,23 @@ private static bool TryParseNativeWordChartNumber(string? text, out double value
439439
return false;
440440
}
441441

442-
private static IEnumerable<(OpenXmlElement SeriesElement, int OriginalSeriesIndex)> GetNativeWordOrderedSeriesElements(OpenXmlElement chartElement) {
443-
return chartElement.ChildElements
444-
.Where(element => element.LocalName == "ser")
445-
.Select((element, index) => (SeriesElement: element, OriginalSeriesIndex: index, Order: GetNativeWordChartSeriesOrder(element, index)))
442+
private static IReadOnlyList<(OpenXmlElement SeriesElement, int OriginalSeriesIndex)> GetNativeWordOrderedSeriesElements(OpenXmlElement chartElement) {
443+
var series = new List<(OpenXmlElement SeriesElement, int OriginalSeriesIndex, int Order)>();
444+
int index = 0;
445+
foreach (OpenXmlElement element in chartElement.ChildElements.Where(element => element.LocalName == "ser")) {
446+
if (series.Count >= MaxNativeWordChartSeries) {
447+
throw new NativeWordChartLimitException("Word chart cache exceeds the maximum supported series count.");
448+
}
449+
450+
series.Add((element, index, GetNativeWordChartSeriesOrder(element, index)));
451+
index++;
452+
}
453+
454+
return series
446455
.OrderBy(item => item.Order)
447456
.ThenBy(item => item.OriginalSeriesIndex)
448-
.Select(item => (item.SeriesElement, item.OriginalSeriesIndex));
457+
.Select(item => (item.SeriesElement, item.OriginalSeriesIndex))
458+
.ToArray();
449459
}
450460

451461
private static int GetNativeWordChartSeriesOrder(OpenXmlElement seriesElement, int fallback) {

0 commit comments

Comments
 (0)