mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 14:22:40 +01:00
Harden file type inputs (#4635)
This commit is contained in:
parent
7ad91a76cd
commit
fea27b900c
7 changed files with 252 additions and 42 deletions
|
@ -581,6 +581,22 @@ namespace BTCPayServer.Tests
|
|||
return new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero) + TimeSpan.FromDays(days);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanDetectImage()
|
||||
{
|
||||
Assert.True(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, "test.bmp"));
|
||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, ".bmp"));
|
||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, "test.svg"));
|
||||
Assert.True(FileTypeDetector.IsPicture(new byte[] { 0xFF, 0xD8, 0xFF, 0xD9 }, "test.jpg"));
|
||||
Assert.True(FileTypeDetector.IsPicture(new byte[] { 0xFF, 0xD8, 0xFF, 0xD9 }, "test.jpeg"));
|
||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0xFF, 0xD8, 0xFF, 0xDA }, "test.jpg"));
|
||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0xFF, 0xD8, 0xFF }, "test.jpg"));
|
||||
Assert.True(FileTypeDetector.IsPicture(new byte[] { 0x3C, 0x73, 0x76, 0x67 }, "test.svg"));
|
||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x3C, 0x73, 0x76, 0x67 }, "test.jpg"));
|
||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0xFF }, "e.jpg"));
|
||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { }, "empty.jpg"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundupCurrenciesCorrectly()
|
||||
{
|
||||
|
|
62
BTCPayServer/BufferizedFormFile.cs
Normal file
62
BTCPayServer/BufferizedFormFile.cs
Normal file
|
@ -0,0 +1,62 @@
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class BufferizedFormFile : IFormFile
|
||||
{
|
||||
private IFormFile _formFile;
|
||||
private MemoryStream _content;
|
||||
public byte[] Buffer { get; }
|
||||
BufferizedFormFile(IFormFile formFile, byte[] content)
|
||||
{
|
||||
_formFile = formFile;
|
||||
Buffer = content;
|
||||
_content = new MemoryStream(content);
|
||||
}
|
||||
|
||||
public string ContentType => _formFile.ContentType;
|
||||
|
||||
public string ContentDisposition => _formFile.ContentDisposition;
|
||||
|
||||
public IHeaderDictionary Headers => _formFile.Headers;
|
||||
|
||||
public long Length => _formFile.Length;
|
||||
|
||||
public string Name => _formFile.Name;
|
||||
|
||||
public string FileName => _formFile.FileName;
|
||||
|
||||
public static async Task<BufferizedFormFile> Bufferize(IFormFile formFile)
|
||||
{
|
||||
if (formFile is BufferizedFormFile b)
|
||||
return b;
|
||||
var content = new byte[formFile.Length];
|
||||
using var fs = formFile.OpenReadStream();
|
||||
await fs.ReadAsync(content, 0, content.Length);
|
||||
return new BufferizedFormFile(formFile, content);
|
||||
}
|
||||
|
||||
public void CopyTo(Stream target)
|
||||
{
|
||||
_content.CopyTo(target);
|
||||
}
|
||||
|
||||
public Task CopyToAsync(Stream target, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _content.CopyToAsync(target, cancellationToken);
|
||||
}
|
||||
|
||||
public Stream OpenReadStream()
|
||||
{
|
||||
return _content;
|
||||
}
|
||||
|
||||
public void Rewind()
|
||||
{
|
||||
_content.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1043,29 +1043,42 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
if (model.LogoFile != null)
|
||||
{
|
||||
if (model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
if (model.LogoFile.Length > 1_000_000)
|
||||
{
|
||||
// delete existing image
|
||||
if (!string.IsNullOrEmpty(settings.LogoFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(settings.LogoFileId, userId);
|
||||
}
|
||||
|
||||
// add new image
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(model.LogoFile, userId);
|
||||
settings.LogoFileId = storedFile.Id;
|
||||
settingsChanged = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.LogoFile), $"Could not save logo: {e.Message}");
|
||||
}
|
||||
TempData[WellKnownTempData.ErrorMessage] = "The uploaded logo file should be less than 1MB";
|
||||
}
|
||||
else if (!model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "The uploaded logo file needs to be an image";
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.LogoFile), "The uploaded logo file needs to be an image");
|
||||
var formFile = await model.LogoFile.Bufferize();
|
||||
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "The uploaded logo file needs to be an image";
|
||||
}
|
||||
else
|
||||
{
|
||||
model.LogoFile = formFile;
|
||||
// delete existing image
|
||||
if (!string.IsNullOrEmpty(settings.LogoFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(settings.LogoFileId, userId);
|
||||
}
|
||||
|
||||
// add new image
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(model.LogoFile, userId);
|
||||
settings.LogoFileId = storedFile.Id;
|
||||
settingsChanged = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.LogoFile), $"Could not save logo: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (RemoveLogoFile && !string.IsNullOrEmpty(settings.LogoFileId))
|
||||
|
|
|
@ -27,6 +27,7 @@ using BTCPayServer.Services.Stores;
|
|||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
@ -658,28 +659,41 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
if (model.LogoFile != null)
|
||||
{
|
||||
if (model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
if (model.LogoFile.Length > 1_000_000)
|
||||
{
|
||||
// delete existing image
|
||||
if (!string.IsNullOrEmpty(blob.LogoFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(blob.LogoFileId, userId);
|
||||
}
|
||||
|
||||
// add new image
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(model.LogoFile, userId);
|
||||
blob.LogoFileId = storedFile.Id;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Could not save logo: {e.Message}";
|
||||
}
|
||||
TempData[WellKnownTempData.ErrorMessage] = "The uploaded logo file should be less than 1MB";
|
||||
}
|
||||
else if (!model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "The uploaded logo file needs to be an image";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "The uploaded logo file needs to be an image";
|
||||
var formFile = await model.LogoFile.Bufferize();
|
||||
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "The uploaded logo file needs to be an image";
|
||||
}
|
||||
else
|
||||
{
|
||||
model.LogoFile = formFile;
|
||||
// delete existing image
|
||||
if (!string.IsNullOrEmpty(blob.LogoFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(blob.LogoFileId, userId);
|
||||
}
|
||||
|
||||
// add new image
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(model.LogoFile, userId);
|
||||
blob.LogoFileId = storedFile.Id;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Could not save logo: {e.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (RemoveLogoFile && !string.IsNullOrEmpty(blob.LogoFileId))
|
||||
|
@ -691,7 +705,19 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
if (model.CssFile != null)
|
||||
{
|
||||
if (model.CssFile.ContentType.Equals("text/css", StringComparison.InvariantCulture))
|
||||
if (model.CssFile.Length > 1_000_000)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "The uploaded file should be less than 1MB";
|
||||
}
|
||||
else if (!model.CssFile.ContentType.Equals("text/css", StringComparison.InvariantCulture))
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "The uploaded file needs to be a CSS file";
|
||||
}
|
||||
else if (!model.CssFile.FileName.EndsWith(".css", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "The uploaded file needs to be a CSS file";
|
||||
}
|
||||
else
|
||||
{
|
||||
// delete existing CSS file
|
||||
if (!string.IsNullOrEmpty(blob.CssFileId))
|
||||
|
@ -710,10 +736,6 @@ namespace BTCPayServer.Controllers
|
|||
TempData[WellKnownTempData.ErrorMessage] = $"Could not save CSS file: {e.Message}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "The uploaded file needs to be a CSS file";
|
||||
}
|
||||
}
|
||||
else if (RemoveCssFile && !string.IsNullOrEmpty(blob.CssFileId))
|
||||
{
|
||||
|
|
|
@ -36,6 +36,10 @@ namespace BTCPayServer
|
|||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static Task<BufferizedFormFile> Bufferize(this IFormFile formFile)
|
||||
{
|
||||
return BufferizedFormFile.Bufferize(formFile);
|
||||
}
|
||||
/// <summary>
|
||||
/// Unescape Uri string for %2F
|
||||
/// See details at: https://github.com/dotnet/aspnetcore/issues/14170#issuecomment-533342396
|
||||
|
|
92
BTCPayServer/FileTypeDetector.cs
Normal file
92
BTCPayServer/FileTypeDetector.cs
Normal file
|
@ -0,0 +1,92 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class FileTypeDetector
|
||||
{
|
||||
// Thanks to https://www.garykessler.net/software/FileSigs_20220731.zip
|
||||
|
||||
const string pictureSigs =
|
||||
"JPEG2000 image files,00 00 00 0C 6A 50 20 20,JP2,Picture,0,(null)\n" +
|
||||
"Bitmap image,42 4D,BMP|DIB,Picture,0,(null)\n" +
|
||||
"GIF file,47 49 46 38,GIF,Picture,0,00 3B\n" +
|
||||
"PNG image,89 50 4E 47 0D 0A 1A 0A,PNG|APNG,Picture,0,49 45 4E 44 AE 42 60 82\n" +
|
||||
"Generic JPEGimage fil,FF D8,JPE|JPEG|JPG,Picture,0,FF D9\n" +
|
||||
"JPEG-EXIF-SPIFF images,FF D8 FF,JFIF|JPE|JPEG|JPG,Picture,0,FF D9\n" +
|
||||
"SVG images, 3C 73 76 67,SVG,Picture,0,(null)\n" +
|
||||
"Google WebP image file, 52 49 46 46 XX XX XX XX 57 45 42 50,WEBP,Picture,0,(null)\n" +
|
||||
"AVIF image file, XX XX XX XX 66 74 79 70,AVIF,Picture,0,(null)\n";
|
||||
|
||||
readonly static (int[] Header, int[]? Trailer, string[] Extensions)[] headerTrailers;
|
||||
static FileTypeDetector()
|
||||
{
|
||||
var lines = pictureSigs.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
headerTrailers = new (int[] Header, int[]? Trailer, string[] Extensions)[lines.Length];
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var cells = lines[i].Split(',');
|
||||
headerTrailers[i] = (
|
||||
DecodeData(cells[1]),
|
||||
cells[^1] == "(null)" ? null : DecodeData(cells[^1]),
|
||||
cells[2].Split('|').Select(p => $".{p}").ToArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static int[] DecodeData(string pattern)
|
||||
{
|
||||
pattern = pattern.Replace(" ", "");
|
||||
int[] res = new int[pattern.Length / 2];
|
||||
for (int i = 0; i < pattern.Length; i+=2)
|
||||
{
|
||||
var b = pattern[i..(i + 2)];
|
||||
if (b == "XX")
|
||||
res[i/2] = -1;
|
||||
else
|
||||
res[i/2] = byte.Parse(b, System.Globalization.NumberStyles.HexNumber);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
public static bool IsPicture(byte[] bytes, string? filename)
|
||||
{
|
||||
for (int i = 0; i < headerTrailers.Length; i++)
|
||||
{
|
||||
if (headerTrailers[i].Header is int[] header)
|
||||
{
|
||||
if (header.Length > bytes.Length)
|
||||
goto next;
|
||||
for (int x = 0; x < header.Length; x++)
|
||||
{
|
||||
if (bytes[x] != header[x] && header[x] != -1)
|
||||
goto next;
|
||||
}
|
||||
}
|
||||
if (headerTrailers[i].Trailer is int[] trailer)
|
||||
{
|
||||
if (trailer.Length > bytes.Length)
|
||||
goto next;
|
||||
for (int x = 0; x < trailer.Length; x++)
|
||||
{
|
||||
if (bytes[^(trailer.Length - x)] != trailer[x] && trailer[x] != -1)
|
||||
goto next;
|
||||
}
|
||||
}
|
||||
|
||||
if (filename is not null)
|
||||
{
|
||||
if (!headerTrailers[i].Extensions.Any(ext => filename.Length > ext.Length && filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase)))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
next:
|
||||
;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -76,6 +76,7 @@ namespace BTCPayServer.Storage
|
|||
context.Context.Response.Headers["Content-Disposition"] = "attachment";
|
||||
}
|
||||
context.Context.Response.Headers["Content-Security-Policy"] = "script-src ;";
|
||||
context.Context.Response.Headers["X-Content-Type-Options"] = "nosniff";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue