mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Harden file type inputs (#4635)
This commit is contained in:
@@ -581,6 +581,22 @@ namespace BTCPayServer.Tests
|
|||||||
return new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero) + TimeSpan.FromDays(days);
|
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]
|
[Fact]
|
||||||
public void RoundupCurrenciesCorrectly()
|
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 != null)
|
||||||
{
|
{
|
||||||
if (model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
if (model.LogoFile.Length > 1_000_000)
|
||||||
{
|
{
|
||||||
// delete existing image
|
TempData[WellKnownTempData.ErrorMessage] = "The uploaded logo file should be less than 1MB";
|
||||||
if (!string.IsNullOrEmpty(settings.LogoFileId))
|
}
|
||||||
{
|
else if (!model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||||
await _fileService.RemoveFile(settings.LogoFileId, userId);
|
{
|
||||||
}
|
TempData[WellKnownTempData.ErrorMessage] = "The uploaded logo file needs to be an image";
|
||||||
|
|
||||||
// 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
|
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))
|
else if (RemoveLogoFile && !string.IsNullOrEmpty(settings.LogoFileId))
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ using BTCPayServer.Services.Stores;
|
|||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
@@ -658,28 +659,41 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
if (model.LogoFile != null)
|
if (model.LogoFile != null)
|
||||||
{
|
{
|
||||||
if (model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
if (model.LogoFile.Length > 1_000_000)
|
||||||
{
|
{
|
||||||
// delete existing image
|
TempData[WellKnownTempData.ErrorMessage] = "The uploaded logo file should be less than 1MB";
|
||||||
if (!string.IsNullOrEmpty(blob.LogoFileId))
|
}
|
||||||
{
|
else if (!model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||||
await _fileService.RemoveFile(blob.LogoFileId, userId);
|
{
|
||||||
}
|
TempData[WellKnownTempData.ErrorMessage] = "The uploaded logo file needs to be an image";
|
||||||
|
|
||||||
// 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
|
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))
|
else if (RemoveLogoFile && !string.IsNullOrEmpty(blob.LogoFileId))
|
||||||
@@ -691,7 +705,19 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
if (model.CssFile != null)
|
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
|
// delete existing CSS file
|
||||||
if (!string.IsNullOrEmpty(blob.CssFileId))
|
if (!string.IsNullOrEmpty(blob.CssFileId))
|
||||||
@@ -710,10 +736,6 @@ namespace BTCPayServer.Controllers
|
|||||||
TempData[WellKnownTempData.ErrorMessage] = $"Could not save CSS file: {e.Message}";
|
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))
|
else if (RemoveCssFile && !string.IsNullOrEmpty(blob.CssFileId))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ namespace BTCPayServer
|
|||||||
{
|
{
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
|
public static Task<BufferizedFormFile> Bufferize(this IFormFile formFile)
|
||||||
|
{
|
||||||
|
return BufferizedFormFile.Bufferize(formFile);
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unescape Uri string for %2F
|
/// Unescape Uri string for %2F
|
||||||
/// See details at: https://github.com/dotnet/aspnetcore/issues/14170#issuecomment-533342396
|
/// 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-Disposition"] = "attachment";
|
||||||
}
|
}
|
||||||
context.Context.Response.Headers["Content-Security-Policy"] = "script-src ;";
|
context.Context.Response.Headers["Content-Security-Policy"] = "script-src ;";
|
||||||
|
context.Context.Response.Headers["X-Content-Type-Options"] = "nosniff";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user