Skip to content

Commit deeb081

Browse files
authored
[PM-36584] Bidirectional C2 in icons.bitwarden.net (#7668)
* adds png chunk verification * changes from fail open to fail closed when png/ico are malformed * removes unused const * fixes tests for stripped png * adds maxEntries for ico * removes redundant count == 0 check
1 parent 061f7d6 commit deeb081

4 files changed

Lines changed: 379 additions & 6 deletions

File tree

src/Icons/Icons.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,8 @@
2222
<ProjectReference Include="..\Core\Core.csproj" />
2323
</ItemGroup>
2424

25+
<ItemGroup>
26+
<InternalsVisibleTo Include="Icons.Test" />
27+
</ItemGroup>
28+
2529
</Project>

src/Icons/Models/IconLink.cs

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#nullable enable
22

3+
using System.Buffers.Binary;
34
using System.Text;
45
using AngleSharp.Dom;
56
using Bit.Icons.Extensions;
@@ -13,25 +14,28 @@ public class IconLink
1314
private static readonly HashSet<string> _blocklistedRels = new(StringComparer.InvariantCultureIgnoreCase) { "preload", "image_src", "preconnect", "canonical", "alternate", "stylesheet" };
1415
private static readonly HashSet<string> _iconExtensions = new(StringComparer.InvariantCultureIgnoreCase) { ".ico", ".png", ".jpg", ".jpeg" };
1516
private const string _pngMediaType = "image/png";
16-
private static readonly byte[] _pngHeader = new byte[] { 137, 80, 78, 71 };
17+
private static readonly byte[] _pngHeader = [137, 80, 78, 71];
18+
private static readonly byte[] _pngSignature = [137, 80, 78, 71, 13, 10, 26, 10];
1719
private static readonly byte[] _webpHeader = Encoding.UTF8.GetBytes("RIFF");
1820

21+
private static readonly HashSet<string> _allowedPngChunkTypes = new(StringComparer.Ordinal)
22+
{
23+
"IHDR", "PLTE", "IDAT", "IEND", "tRNS", "sRGB", "gAMA", "cHRM",
24+
};
25+
1926
private const string _icoMediaType = "image/x-icon";
2027
private const string _icoAltMediaType = "image/vnd.microsoft.icon";
2128
private static readonly byte[] _icoHeader = new byte[] { 00, 00, 01, 00 };
2229

2330
private const string _jpegMediaType = "image/jpeg";
2431
private static readonly byte[] _jpegHeader = new byte[] { 255, 216, 255 };
2532

26-
private const string _svgXmlMediaType = "image/svg+xml";
27-
2833
private static readonly HashSet<string> _allowedMediaTypes = new(StringComparer.InvariantCultureIgnoreCase)
2934
{
3035
_pngMediaType,
3136
_icoMediaType,
3237
_icoAltMediaType,
3338
_jpegMediaType,
34-
_svgXmlMediaType,
3539
};
3640

3741
private bool _useUriDirectly = false;
@@ -156,6 +160,20 @@ public bool IsUsable()
156160
return null;
157161
}
158162

163+
if (HeaderMatch(bytes, _pngHeader))
164+
{
165+
bytes = StripPngMetadata(bytes);
166+
}
167+
else if (HeaderMatch(bytes, _icoHeader))
168+
{
169+
bytes = StripIcoEmbeddedPngMetadata(bytes);
170+
}
171+
172+
if (bytes == null)
173+
{
174+
return null;
175+
}
176+
159177
return new Icon { Image = bytes, Format = format };
160178
}
161179

@@ -217,4 +235,126 @@ private static string DetermineImageFormatFromFile(byte[] imageBytes)
217235
return string.Empty;
218236
}
219237
}
238+
239+
internal static byte[]? StripPngMetadata(byte[] bytes)
240+
{
241+
if (bytes.Length < _pngSignature.Length || !HeaderMatch(bytes, _pngSignature))
242+
{
243+
return null;
244+
}
245+
246+
using var output = new MemoryStream(bytes.Length);
247+
output.Write(bytes, 0, _pngSignature.Length);
248+
249+
var offset = _pngSignature.Length;
250+
var seenIend = false;
251+
while (offset + 12 <= bytes.Length)
252+
{
253+
var length = BinaryPrimitives.ReadInt32BigEndian(bytes.AsSpan(offset, 4));
254+
if (length < 0 || (long)offset + 12 + length > bytes.Length)
255+
{
256+
return null;
257+
}
258+
259+
var type = Encoding.ASCII.GetString(bytes, offset + 4, 4);
260+
var chunkSize = 12 + length;
261+
262+
if (_allowedPngChunkTypes.Contains(type))
263+
{
264+
output.Write(bytes, offset, chunkSize);
265+
}
266+
267+
offset += chunkSize;
268+
269+
if (type == "IEND")
270+
{
271+
seenIend = true;
272+
break;
273+
}
274+
}
275+
276+
if (!seenIend)
277+
{
278+
return null;
279+
}
280+
281+
return output.ToArray();
282+
}
283+
284+
internal static byte[]? StripIcoEmbeddedPngMetadata(byte[] bytes)
285+
{
286+
const int dirHeaderSize = 6;
287+
const int entrySize = 16;
288+
289+
if (bytes.Length < dirHeaderSize || !HeaderMatch(bytes, _icoHeader))
290+
{
291+
return null;
292+
}
293+
294+
const int maxEntries = 64;
295+
var count = BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(4, 2));
296+
if (count == 0 || count > maxEntries)
297+
{
298+
return null;
299+
}
300+
301+
var directorySize = dirHeaderSize + count * entrySize;
302+
if (bytes.Length < directorySize)
303+
{
304+
return null;
305+
}
306+
307+
var entries = new byte[count][];
308+
var images = new byte[count][];
309+
310+
for (var i = 0; i < count; i++)
311+
{
312+
var entryOffset = dirHeaderSize + i * entrySize;
313+
var size = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(entryOffset + 8, 4));
314+
var dataOffset = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(entryOffset + 12, 4));
315+
316+
if (size < 0 || dataOffset < directorySize || (long)dataOffset + size > bytes.Length)
317+
{
318+
return null;
319+
}
320+
321+
var image = new byte[size];
322+
Buffer.BlockCopy(bytes, dataOffset, image, 0, size);
323+
324+
if (HeaderMatch(image, _pngHeader))
325+
{
326+
var stripped = StripPngMetadata(image);
327+
if (stripped == null)
328+
{
329+
return null;
330+
}
331+
image = stripped;
332+
}
333+
334+
var entry = new byte[entrySize];
335+
Buffer.BlockCopy(bytes, entryOffset, entry, 0, entrySize);
336+
337+
entries[i] = entry;
338+
images[i] = image;
339+
}
340+
341+
using var output = new MemoryStream(bytes.Length);
342+
output.Write(bytes, 0, dirHeaderSize);
343+
344+
var imageOffset = directorySize;
345+
for (var i = 0; i < count; i++)
346+
{
347+
BinaryPrimitives.WriteInt32LittleEndian(entries[i].AsSpan(8, 4), images[i].Length);
348+
BinaryPrimitives.WriteInt32LittleEndian(entries[i].AsSpan(12, 4), imageOffset);
349+
output.Write(entries[i], 0, entrySize);
350+
imageOffset += images[i].Length;
351+
}
352+
353+
for (var i = 0; i < count; i++)
354+
{
355+
output.Write(images[i], 0, images[i].Length);
356+
}
357+
358+
return output.ToArray();
359+
}
220360
}

test/Icons.Test/Models/IconHttpResponseTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,11 @@ private static IHttpClientFactory UsableIconHttpClientFactory()
9393
var handler = new MockedHttpMessageHandler();
9494
handler.Fallback
9595
.WithStatusCode(HttpStatusCode.OK)
96-
.WithContent("image/png", new byte[] { 137, 80, 78, 71 });
96+
.WithContent("image/png", new byte[]
97+
{
98+
137, 80, 78, 71, 13, 10, 26, 10,
99+
0, 0, 0, 0, 73, 69, 78, 68, 0, 0, 0, 0,
100+
});
97101

98102
substitute.CreateClient("Icons").Returns(handler.ToHttpClient());
99103
return substitute;

0 commit comments

Comments
 (0)