Use Safe.Raw and Safe.Json instead of Html.Raw and the JsonHelper, move sanitization at the View level (#960)

This commit is contained in:
Nicolas Dorier 2019-08-10 14:05:11 +09:00 committed by GitHub
parent 6b355cbe1b
commit be5597085b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 132 additions and 82 deletions

View file

@ -34,7 +34,7 @@
<PackageReference Include="BuildBundlerMinifier" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.Core" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="2.9.406" />
<PackageReference Include="HtmlSanitizer" Version="4.0.207" />
<PackageReference Include="HtmlSanitizer" Version="4.0.217" />
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
@ -47,7 +47,7 @@
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.7" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.9" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.2" />
<PackageReference Include="OpenIddict" Version="2.0.0" />

View file

@ -183,7 +183,7 @@ namespace BTCPayServer.Controllers
{
Version = u2fChallenge[0].version,
Challenge = u2fChallenge[0].challenge,
Challenges = JsonConvert.SerializeObject(u2fChallenge),
Challenges = u2fChallenge,
AppId = u2fChallenge[0].appId,
UserId = user.Id,
RememberMe = rememberMe

View file

@ -132,7 +132,7 @@ namespace BTCPayServer.Controllers
EnforceTargetAmount = vm.EnforceTargetAmount,
StartDate = vm.StartDate?.ToUniversalTime(),
TargetCurrency = vm.TargetCurrency,
Description = _htmlSanitizer.Sanitize( vm.Description),
Description = vm.Description,
EndDate = vm.EndDate?.ToUniversalTime(),
TargetAmount = vm.TargetAmount,
CustomCSSLink = vm.CustomCSSLink,

View file

@ -30,7 +30,6 @@ namespace BTCPayServer.Controllers
EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider,
CurrencyNameTable currencies,
HtmlSanitizer htmlSanitizer,
EmailSenderFactory emailSenderFactory,
AppService AppService)
{
@ -39,7 +38,6 @@ namespace BTCPayServer.Controllers
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_currencies = currencies;
_htmlSanitizer = htmlSanitizer;
_emailSenderFactory = emailSenderFactory;
_AppService = AppService;
}
@ -49,7 +47,6 @@ namespace BTCPayServer.Controllers
private readonly EventAggregator _EventAggregator;
private BTCPayNetworkProvider _NetworkProvider;
private readonly CurrencyNameTable _currencies;
private readonly HtmlSanitizer _htmlSanitizer;
private readonly EmailSenderFactory _emailSenderFactory;
private AppService _AppService;

View file

@ -3,6 +3,7 @@ using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Models.ManageViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@ -11,6 +12,7 @@ namespace BTCPayServer.Controllers
{
public partial class ManageController
{
private const string RecoveryCodesKey = nameof(RecoveryCodesKey);
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
[HttpGet]
@ -80,18 +82,8 @@ namespace BTCPayServer.Controllers
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(unformattedKey))
{
await _userManager.ResetAuthenticatorKeyAsync(user);
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
var model = new EnableAuthenticatorViewModel
{
SharedKey = FormatKey(unformattedKey),
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey)
};
var model = new EnableAuthenticatorViewModel();
await LoadSharedKeyAndQrCodeUriAsync(user, model);
return View(model);
}
@ -100,32 +92,36 @@ namespace BTCPayServer.Controllers
[ValidateAntiForgeryToken]
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!ModelState.IsValid)
{
await LoadSharedKeyAndQrCodeUriAsync(user, model);
return View(model);
}
// Strip spaces and hypens
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture)
.Replace("-", string.Empty, StringComparison.InvariantCulture);
var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
if (!is2faTokenValid)
{
ModelState.AddModelError(nameof(model.Code), "Verification code is invalid.");
ModelState.AddModelError("Code", "Verification code is invalid.");
await LoadSharedKeyAndQrCodeUriAsync(user, model);
return View(model);
}
await _userManager.SetTwoFactorEnabledAsync(user, true);
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
TempData[RecoveryCodesKey] = recoveryCodes.ToArray();
return RedirectToAction(nameof(GenerateRecoveryCodes));
}
@ -153,7 +149,20 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
public async Task<IActionResult> GenerateRecoveryCodes()
public IActionResult GenerateRecoveryCodes()
{
var recoveryCodes = (string[])TempData[RecoveryCodesKey];
if (recoveryCodes == null)
{
return RedirectToAction(nameof(TwoFactorAuthentication));
}
var model = new GenerateRecoveryCodesViewModel {RecoveryCodes = recoveryCodes};
return View(model);
}
[HttpGet]
public async Task<IActionResult> GenerateRecoveryCodesWarning()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
@ -163,16 +172,10 @@ namespace BTCPayServer.Controllers
if (!user.TwoFactorEnabled)
{
throw new ApplicationException(
$"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' because they do not have 2FA enabled.");
}
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
var model = new GenerateRecoveryCodesViewModel {RecoveryCodes = recoveryCodes.ToArray()};
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
return View(model);
return View(nameof(GenerateRecoveryCodesWarning));
}
private string GenerateQrCodeUri(string email, string unformattedKey)
@ -201,5 +204,19 @@ namespace BTCPayServer.Controllers
return result.ToString().ToLowerInvariant();
}
private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user, EnableAuthenticatorViewModel model)
{
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(unformattedKey))
{
await _userManager.ResetAuthenticatorKeyAsync(user);
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
model.SharedKey = FormatKey(unformattedKey);
model.AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey);
}
}
}

View file

@ -41,7 +41,6 @@ namespace BTCPayServer.Controllers
private readonly PaymentRequestService _PaymentRequestService;
private readonly EventAggregator _EventAggregator;
private readonly CurrencyNameTable _Currencies;
private readonly HtmlSanitizer _htmlSanitizer;
private readonly InvoiceRepository _InvoiceRepository;
public PaymentRequestController(
@ -52,7 +51,6 @@ namespace BTCPayServer.Controllers
PaymentRequestService paymentRequestService,
EventAggregator eventAggregator,
CurrencyNameTable currencies,
HtmlSanitizer htmlSanitizer,
InvoiceRepository invoiceRepository)
{
_InvoiceController = invoiceController;
@ -62,7 +60,6 @@ namespace BTCPayServer.Controllers
_PaymentRequestService = paymentRequestService;
_EventAggregator = eventAggregator;
_Currencies = currencies;
_htmlSanitizer = htmlSanitizer;
_InvoiceRepository = invoiceRepository;
}
@ -152,7 +149,7 @@ namespace BTCPayServer.Controllers
blob.Title = viewModel.Title;
blob.Email = viewModel.Email;
blob.Description = _htmlSanitizer.Sanitize(viewModel.Description);
blob.Description = viewModel.Description;
blob.Amount = viewModel.Amount;
blob.ExpiryDate = viewModel.ExpiryDate?.ToUniversalTime();
blob.Currency = viewModel.Currency;

View file

@ -126,6 +126,7 @@ namespace BTCPayServer.Hosting
});
services.TryAddSingleton<AppService>();
services.TryAddTransient<Safe>();
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
{

View file

@ -4,6 +4,7 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace BTCPayServer.Models.ManageViewModels
{
@ -15,9 +16,10 @@ namespace BTCPayServer.Models.ManageViewModels
[Display(Name = "Verification Code")]
public string Code { get; set; }
[ReadOnly(true)]
[BindNever]
public string SharedKey { get; set; }
[BindNever]
public string AuthenticatorUri { get; set; }
}
}

View file

@ -283,10 +283,10 @@ namespace BTCPayServer.Services.Apps
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Description = _HtmlSanitizer.Sanitize(c.GetDetailString("description")),
Description = c.GetDetailString("description"),
Id = c.Key,
Image = _HtmlSanitizer.Sanitize(c.GetDetailString("image")),
Title = _HtmlSanitizer.Sanitize(c.GetDetailString("title") ?? c.Key),
Image = c.GetDetailString("image"),
Title = c.GetDetailString("title") ?? c.Key,
Price = c.GetDetail("price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{

View file

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Ganss.XSS;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Services
{
public class Safe
{
private readonly IHtmlHelper _htmlHelper;
private readonly IJsonHelper _jsonHelper;
private readonly HtmlSanitizer _htmlSanitizer;
public Safe(IHtmlHelper htmlHelper, IJsonHelper jsonHelper, HtmlSanitizer htmlSanitizer)
{
_htmlHelper = htmlHelper;
_jsonHelper = jsonHelper;
_htmlSanitizer = htmlSanitizer;
}
public IHtmlContent Raw(string value)
{
return _htmlHelper.Raw(_htmlSanitizer.Sanitize(value));
}
public IHtmlContent Json(object model)
{
return _htmlHelper.Raw(_jsonHelper.Serialize(model));
}
}
}

View file

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace BTCPayServer.Services.U2F.Models
@ -18,7 +19,7 @@ namespace BTCPayServer.Services.U2F.Models
public string DeviceResponse { get; set; }
[Display(Name = "Challenges")]
public string Challenges { get; set; }
public List<ServerChallenge> Challenges { get; set; }
[Display(Name = "Challenge")]
public string Challenge { get; set; }

View file

@ -37,9 +37,9 @@
};
setTimeout(function() {
window.u2f.sign(
"@Model.AppId",
"@Model.Challenge",
@Html.Raw(@Model.Challenges), function (data) {
@Safe.Json(Model.AppId),
@Safe.Json(Model.Challenge),
@Safe.Json(Model.Challenges), function (data) {
if (data.errorCode) {
$("#error-response").text(errorMap[data.errorCode]);
return;

View file

@ -30,17 +30,17 @@
@item.Price.Value
if (item.Custom)
{
Html.Raw("or more");
Safe.Raw("or more");
}
}
else if (item.Custom)
{
Html.Raw("Any amount");
Safe.Raw("Any amount");
}
</span>
</div>
<p class="card-text overflow-hidden">@Html.Raw(item.Description)</p>
<p class="card-text overflow-hidden">@Safe.Raw(item.Description)</p>
</div>
@if (Model.ViewCrowdfundViewModel.PerkCount.ContainsKey(item.Id))

View file

@ -131,7 +131,7 @@
<hr/>
<div class="row">
<div class="col-md-8 col-sm-12">
<div class="card-text overflow-hidden">@Html.Raw(Model.Description)</div>
<div class="card-text overflow-hidden">@Safe.Raw(Model.Description)</div>
</div>
<div class="col-md-4 col-sm-12">
<partial

View file

@ -21,7 +21,7 @@
@if (!Context.Request.Query.ContainsKey("simple"))
{
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
var srvModel = @Safe.Json(Model);
</script>
<bundle name="wwwroot/bundles/crowdfund-bundle-1.min.js"></bundle>
<bundle name="wwwroot/bundles/crowdfund-bundle-2.min.js"></bundle>
@ -33,7 +33,7 @@
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{
<style>
@Html.Raw(Model.EmbeddedCSS);
@Safe.Raw(Model.EmbeddedCSS);
</style>
}

View file

@ -31,7 +31,7 @@
{
<link rel="stylesheet" href="~/cart/css/style.css">
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
var srvModel = @Safe.Json(Model);
</script>
<bundle name="wwwroot/bundles/cart-bundle.min.js" />
}

View file

@ -10,7 +10,7 @@
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<p>You need to pay <b>@Model.Amount</b> to <b>@Model.Address</b></p>
<div id="qrCode"></div>
<div id="qrCodeData" data-url="@Html.Raw(Model.BitcoinUri)" style="margin-bottom:20px;"></div>
<div id="qrCodeData" data-url="@Model.BitcoinUri" style="margin-bottom:20px;"></div>
<p>
<a class="btn btn-primary" href="@Model.BitcoinUri">
<span>Open in wallet</span>
@ -52,7 +52,7 @@
<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: "@Html.Raw(Model.BitcoinUri)",
text: @Safe.Json(Model.BitcoinUri),
width: 200,
height: 200,
useSVG: true

View file

@ -19,7 +19,7 @@
<bundle name="wwwroot/bundles/checkout-bundle.min.css" />
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
var srvModel = @Safe.Json(Model);
</script>
<bundle name="wwwroot/bundles/checkout-bundle.min.js" />
@ -114,8 +114,8 @@
</div>
</invoice>
<script type="text/javascript">
var availableLanguages = @Html.Raw(Json.Serialize(langService.GetLanguages().Select((language) => language.Code)));;
var storeDefaultLang = "@Model.DefaultLang";
var availableLanguages = @Safe.Json(langService.GetLanguages().Select((language) => language.Code));;
var storeDefaultLang = @Safe.Json(@Model.DefaultLang);
var fallbackLanguage = "en";
startingLanguage = computeStartingLanguage();
// initialization
@ -123,7 +123,7 @@
.use(window.i18nextXHRBackend)
.init({
backend: {
loadPath: '@(Model.RootPath)locales/{{lng}}.json'
loadPath: @Safe.Json($"{Model.RootPath}locales/{{{{lng}}}}.json")
},
lng: startingLanguage,
fallbackLng: fallbackLanguage,

View file

@ -39,7 +39,7 @@
};
setTimeout(function() {
var request = { "challenge": "@Model.Challenge", "version": "@Model.Version", "appId": "@Model.AppId" };
var request = { "challenge": @Safe.Json(Model.Challenge), "version": @Safe.Json(Model.Version), "appId": @Safe.Json(Model.AppId) };
var registerRequests = [{version: request.version, challenge: request.challenge}];
u2f.register(request.appId, registerRequests, [],
function(data) {

View file

@ -20,7 +20,7 @@
<li>
<p>Scan the QR Code or enter this key <kbd>@Model.SharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
<div id="qrCode"></div>
<div id="qrCodeData" data-url="@Html.Raw(Model.AuthenticatorUri)"></div>
<div id="qrCodeData" data-url="@Model.AuthenticatorUri"></div>
<br />
</li>
<li>
@ -53,7 +53,7 @@
<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: "@Html.Raw(Model.AuthenticatorUri)",
text: @Safe.Json(Model.AuthenticatorUri),
width: 200,
height: 200,
useSVG: true

View file

@ -31,7 +31,7 @@
</div>
</li>
</ul>
<div class="w-100 p-2">@Html.Raw(Model.Description)</div>
<div class="w-100 p-2">@Safe.Raw(Model.Description)</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">

View file

@ -22,7 +22,7 @@
@if (!Context.Request.Query.ContainsKey("simple"))
{
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
var srvModel = @Safe.Json(Model);
</script>
<bundle name="wwwroot/bundles/payment-request-bundle-1.min.js"></bundle>
<bundle name="wwwroot/bundles/payment-request-bundle-2.min.js"></bundle>
@ -34,7 +34,7 @@
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{
<style>
@Html.Raw(Model.EmbeddedCSS);
@Safe.Raw(Model.EmbeddedCSS);
</style>
}
</head>

View file

@ -23,7 +23,7 @@
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js" />
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
var srvModel = @Safe.Json(Model);
window.onload = function() {

View file

@ -54,7 +54,7 @@
{
<div class="form-group">
<div id="qrCode"></div>
<div id="qrCodeData" data-url="@Html.Raw(Model.ServiceLink)"></div>
<div id="qrCodeData" data-url="@Model.ServiceLink"></div>
</div>
}
</div>
@ -70,7 +70,7 @@
<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: "@Html.Raw(Model.ServiceLink)",
text: @Safe.Json(Model.ServiceLink),
width: 200,
height: 200,
useSVG: true

View file

@ -90,7 +90,7 @@
{
<div class="form-group">
<div id="qrCode"></div>
<div id="qrCodeData" data-url="@Html.Raw(Model.QRCode)"></div>
<div id="qrCodeData" data-url="@Model.QRCode"></div>
</div>
<p>See QR Code information by clicking <a href="#detailsQR" data-toggle="collapse">here</a></p>
<div id="detailsQR" class="collapse">
@ -184,7 +184,7 @@
<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: "@Html.Raw(Model.QRCode)",
text: @Safe.Json(Model.QRCode),
width: 200,
height: 200,
useSVG: true

View file

@ -78,7 +78,7 @@
{
<div class="form-group">
<div id="qrCode"></div>
<div id="qrCodeData" data-url="@Html.Raw(Model.ServiceLink)"></div>
<div id="qrCodeData" data-url="@Model.ServiceLink"></div>
</div>
<p>See QR Code information by clicking <a href="#detailsQR" data-toggle="collapse">here</a></p>
<div id="detailsQR" class="collapse">
@ -101,7 +101,7 @@
<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: "@Html.Raw(Model.ServiceLink)",
text: @Safe.Json(Model.ServiceLink),
width: 200,
height: 200,
useSVG: true

View file

@ -128,14 +128,14 @@
@RenderSection("Scripts", required: false)
<script type="text/javascript">
var expectedDomain = @Html.Raw(Json.Serialize(env.ExpectedHost));
var expectedProtocol = @Html.Raw(Json.Serialize(env.ExpectedProtocol));
<script type="text/javascript">
var expectedDomain = @Safe.Json(env.ExpectedHost);
var expectedProtocol = @Safe.Json(env.ExpectedProtocol);
if (window.location.host != expectedDomain || window.location.protocol != expectedProtocol + ":") {
document.getElementById("badUrl").style.display = "block";
document.getElementById("browserScheme").innerText = window.location.protocol.substr(0, window.location.protocol.length -1);
}
</script>
</script>
</body>
</html>

View file

@ -16,7 +16,7 @@
}
@if (!string.IsNullOrEmpty(parsedModel.Html))
{
@Html.Raw(parsedModel.Html)
@Safe.Raw(parsedModel.Html)
}
</div>
}

View file

@ -209,7 +209,7 @@
@section Scripts {
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
var srvModel = @Safe.Json(Model);
var payButtonCtrl = new Vue({
el: '#payButtonCtrl',

View file

@ -171,6 +171,6 @@
</div>
@section Scripts {
<script type="text/javascript">var defaultScript = @Html.Raw(Json.Serialize(Model.DefaultScript));</script>
<script type="text/javascript">var defaultScript = @Safe.Json(Model.DefaultScript);</script>
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View file

@ -6,4 +6,5 @@
@using BTCPayServer.Models.InvoicingModels
@using BTCPayServer.Models.ManageViewModels
@using BTCPayServer.Models.StoreViewModels
@inject BTCPayServer.Services.Safe Safe
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View file

@ -47,5 +47,5 @@
"Pay with CoinSwitch": "CoinSwitchでのお支払い",
"Pay with Changelly": "Changellyでのお支払い",
"Close": "閉じる",
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due."
"NotPaid_ExtraTransaction": "請求金額の全額が支払われていません。未払い分の別のトランザクションをお送りください。"
}