In a .net 6 MVC site I’m using cropper.js to crop photos. It works on my local machine, but not on my server (IIS 10)

I’m using cropper.js in an MVC .net 6 app.

It works on my local machine but not on IIS.

_Layout.cshtml:

@using Microsoft.AspNetCore.Identity
@using Memayo.ViewModels
@using Microsoft.AspNetCore.Http
@using System.Security.Claims
@inject SignInManager<AppUser> SignInManager
@inject UserManager<AppUser> UserManager
@inject IHttpContextAccessor HttpContextAccessor
@{
    var userId = HttpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
    var user = userId != null ? await UserManager.FindByIdAsync(userId) : null;
    var avatarUrl = user?.Avatar != null ? $"/images/uploads/small/{user.Avatar}" : "/images/default-avatar.png";
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Memayo</title>
    <link rel="stylesheet" href="~/bootstrap/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" />
    <link href="~/css/memayo.css" rel="stylesheet" />

@*     <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-eval';"> *@

@*     <link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.css" rel="stylesheet" /> *@    
 <link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.css" rel="stylesheet" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="nav-linkxx xxtext-dark" asp-controller="Home" asp-action="Index">
                    <img class="memayologo" src="~/images/logo.png" />
                </a>

                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>

                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        @if (SignInManager.IsSignedIn(User))
                        {
                            @if (User.IsInRole("admin"))
                            {
                                <li class="nav-item">
                                    <a class="nav-link text-lightx text-danger" asp-area="venus" asp-controller="AHome" asp-action="Index">Admin Area</a>
                                </li>
                            }
                            else
                            {
                                <li class="nav-item">
                                    <a class="nav-link" asp-controller="Yarn" asp-action="Index">Yarn</a>
                                </li>
                                <li class="nav-item">
                                    <a class="nav-link" asp-controller="Question" asp-action="Index">Chapters</a>
                                </li>
                                <li class="nav-item">
                                    <a class="nav-link" asp-controller="Photo" asp-action="Index">Photos</a>
                                </li>
                                <li class="nav-item">
                                    <a class="nav-link" asp-controller="YourQuestion" asp-action="Index">My Q&A</a>
                                </li>
                                <li class="nav-item dropdown">
                                    <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">Friends</a>
                                    <ul class="dropdown-menu">
                                        <li><a class="dropdown-item" asp-controller="Friend" asp-action="Index">Friends List</a></li>
                                        <li><a class="dropdown-item" asp-controller="Friend" asp-action="Requests">Friend Requests</a></li>
                                        <li><a class="dropdown-item" asp-controller="Friend" asp-action="Search">Search for Friends</a></li>
                                    </ul>
                                </li>
                            }
                        }
                    </ul>

                    <ul class="navbar-nav">
                        @if (SignInManager.IsSignedIn(User))
                        {
                            <li class="nav-item dropdown">
                                <a class="nav-link d-flex align-items-center gap-2" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                                    <div class="rounded-circle overflow-hidden" style="width: 38px; height: 38px;">
                                        @if (user?.Avatar != null)
                                        {
                                            <img src="@avatarUrl" alt="User Avatar" class="w-100 h-100 object-fit-cover" />
                                        }
                                        else
                                        {
                                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" class="w-100 h-100">
                                                <circle cx="64" cy="64" r="64" fill="#E2E8F0" />
                                                <path d="M64 36c7.732 0 14 6.268 14 14s-6.268 14-14 14-14-6.268-14-14 6.268-14 14-14zM64 70c18.778 0 34 8.222 34 20v8c0 2-2 2-2 2H32s-2 0-2-2v-8c0-11.778 15.222-20 34-20z" fill="#94A3B8" />
                                            </svg>
                                        }
                                    </div>
                                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-down" viewBox="0 0 16 16">
                                        <path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z" />
                                    </svg>
                                </a>
                                <ul class="dropdown-menu dropdown-menu-end">
                                    <li><a class="dropdown-item" asp-controller="MyProfile" asp-action="Profile">Profile</a></li>
                                    <li><a class="dropdown-item" asp-controller="MyProfile" asp-action="Avatar">Avatar</a></li>
                                    <li><hr class="dropdown-divider"></li>
                                    <li><a class="dropdown-item" asp-controller="account" asp-action="Logout">Logout</a></li>
                                </ul>
                            </li>
                        }
                        else
                        {
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-controller="account" asp-action="Register">Register</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-controller="account" asp-action="Login">Login</a>
                            </li>
                        }
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            <partial name="_MessagePartial" />
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            <partial name="_VersionPartial" />
            <a asp-controller="Home" asp-action="Privacy">Privacy</a> |
            <a asp-controller="Home" asp-action="Terms">Terms</a>
        </div>
    </footer>
    <script src="~/js/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/bootstrap/js/bootstrap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.js"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

Controller:

using CSharpAwsSesServiceHelper.EmailService;
using Memayo.Models;
using Memayo.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using System.Collections.Generic;
using Microsoft.Extensions.Hosting;
using Memayo.Repositories;
using Microsoft.AspNetCore.Http;
using System.IO;
using System.Linq;
using System;

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

namespace Memayo.Controllers
{
    [Authorize(Roles = "user")]
    public class MyProfileController : Controller
    {
        private readonly ILogger<MyProfileController> _logger;
        private IWebHostEnvironment _env;
        private SignInManager<AppUser> _signManager;
        private UserManager<AppUser> _userManager;
        private readonly IEmailService _emailService;
        private readonly IUserRepository _userRepository;
        private readonly IHttpContextAccessor _httpContextAccessor;

        private string userId => _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;

        public MyProfileController(ILogger<MyProfileController> logger, IWebHostEnvironment env,
            UserManager<AppUser> userManager, SignInManager<AppUser> signManager,
            IEmailService emailService, IUserRepository userRepository,
            IHttpContextAccessor httpContextAccessor)
        {
            _logger = logger;
            _env = env;
            _userManager = userManager;
            _signManager = signManager;
            _emailService = emailService;
            _userRepository = userRepository;
            _httpContextAccessor = httpContextAccessor;
        }

        public IActionResult Index()
        {
            return View();
        }

        public async Task<IActionResult> Profile()
        {
            var user = await _userRepository.GetAsync(userId);
            return View(user);
        }

        [HttpPost]
        public async Task<IActionResult> Profile(User user)
        {
            await _userRepository.UpdateProfileAsync(user);
            TempData["message"] = "Profile Updated";
            return RedirectToAction("Index", "Home");
        }

        [Authorize(Roles = "user")]
        public async Task<IActionResult> Avatar()
        {
            var user = await _userRepository.GetAsync(userId);
            return View(user);
        }

        [HttpPost]
        [Authorize(Roles = "user")]
        public async Task<IActionResult> Avatar(IFormFile avatar)
        {
            try
            {
                if (avatar == null)
                {
                    _logger.LogError("Avatar is null");
                    return Json(new { success = false, error = "No file received" });
                }

                _logger.LogInformation($"Received file: {avatar.FileName}, Length: {avatar.Length}, ContentType: {avatar.ContentType}");

                if (avatar.Length == 0)
                {
                    TempData["messageError"] = "Please upload a valid image.";
                    return Json(new { success = false, error = "Empty file" });
                }

                // Check file type
                var allowedExtensions = new[] { ".png", ".jpg", ".jpeg" };
                var extension = Path.GetExtension(avatar.FileName).ToLower();
                if (!allowedExtensions.Contains(extension))
                {
                    TempData["messageError"] = "Only PNG and JPG images are allowed.";
                    return Json(new { success = false, error = "Invalid file type" });
                }

                // Delete old avatar if exists, get user
                var user = await _userRepository.GetAsync(userId);

                var uploadsDir = Path.Combine(_env.WebRootPath, "images", "uploads");
                var filename = $"{Guid.NewGuid()}{extension}";
                var oldAvatarPathLarge = Path.Combine(uploadsDir, "large", user.Avatar);
                var oldAvatarPathSmall = Path.Combine(uploadsDir, "small", user.Avatar);
                var newAvatarPathLarge = Path.Combine(uploadsDir, "large", filename);
                var newAvatarPathSmall = Path.Combine(uploadsDir, "small", filename);

                _logger.LogInformation($"Paths: New Large: {newAvatarPathLarge}, New Small: {newAvatarPathSmall}");

                // Delete old avatar if exists
                if (System.IO.File.Exists(oldAvatarPathLarge))
                {
                    try
                    {
                        System.IO.File.Delete(oldAvatarPathLarge);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError($"Error deleting old large avatar: {ex.Message}");
                    }
                }

                if (System.IO.File.Exists(oldAvatarPathSmall))
                {
                    try
                    {
                        System.IO.File.Delete(oldAvatarPathSmall);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError($"Error deleting old small avatar: {ex.Message}");
                    }
                }

                // Resize the image
                using (var image = await Image.LoadAsync(avatar.OpenReadStream()))
                {
                    _logger.LogInformation("Image loaded successfully");

                    var maxWidthLarge = 1000;
                    var maxHeightLarge = 1000;
                    var maxWidthSmall = 200;
                    var maxHeightSmall = 200;

                    var largeImage = image.Clone(ctx => ctx.Resize(new ResizeOptions
                    {
                        Size = new SixLabors.ImageSharp.Size(maxWidthLarge, maxHeightLarge),
                        Mode = ResizeMode.Max
                    }));

                    var smallImage = image.Clone(ctx => ctx.Resize(new ResizeOptions
                    {
                        Size = new SixLabors.ImageSharp.Size(maxWidthSmall, maxHeightSmall),
                        Mode = ResizeMode.Max
                    }));

                    _logger.LogInformation("Images resized");

                    try
                    {
                        if (extension == ".png")
                        {
                            await largeImage.SaveAsync(newAvatarPathLarge, new PngEncoder());
                            await smallImage.SaveAsync(newAvatarPathSmall, new PngEncoder());
                        }
                        else
                        {
                            await largeImage.SaveAsync(newAvatarPathLarge, new JpegEncoder());
                            await smallImage.SaveAsync(newAvatarPathSmall, new JpegEncoder());
                        }
                        _logger.LogInformation("Images saved successfully");
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError($"Error saving images: {ex.Message}nStack: {ex.StackTrace}");
                        throw;
                    }
                }

                // Update the user's avatar path in the database
                user.Avatar = filename;
                await _userRepository.UpdateAvatarAsync(user);
                _logger.LogInformation("Database updated successfully");

                TempData["message"] = "Avatar updated successfully.";
                return Json(new { success = true });
            }
            catch (Exception ex)
            {
                _logger.LogError($"Avatar upload error: {ex.Message}nStack: {ex.StackTrace}");
                return Json(new { success = false, error = ex.Message });
            }
        }
    }
}

View:

@model Memayo.Models.User
@{
    ViewData["Title"] = "Edit Avatar";
}
<h2>Edit Avatar</h2>
<div class="avatar-section">
    <h4>Current Avatar</h4>
    <div class="current-avatar">
        @if (!string.IsNullOrEmpty(Model.Avatar))
        {
            <img src="/images/uploads/large/@Model.Avatar" alt="User Avatar" class="rounded-circle" style="max-width: 200px; max-height: 200px;" />
        }
        else
        {
            <p>No avatar uploaded. Please upload an avatar.</p>
        }
    </div>
</div>
<hr />
<div class="upload-section">
    @if (TempData["message"] != null)
    {
        <div class="alert alert-success">@TempData["message"]</div>
    }
    @if (TempData["messageError"] != null)
    {
        <div class="alert alert-danger">@TempData["messageError"]</div>
    }
    <form id="avatarForm" method="post" enctype="multipart/form-data">
        <div class="form-group">
            <label for="avatar">Select New Avatar</label>
            <input type="file" class="form-control" id="avatar" name="avatar" accept=".jpg,.jpeg,.png" />
            <small class="form-text text-muted">Allowed: .jpg, .png</small>
        </div>
        <div class="form-group">
            <img id="preview" class="img-fluid" />
        </div>
        <button type="button" class="btn btn-primary" id="cropButton">Crop and Upload Avatar</button>
        <a asp-controller="Home" asp-action="Index" class="btn btn-secondary">Cancel</a>
    </form>
</div>

@section Scripts {
    <script type="module">
        // Initialize state
        const state = {
            cropper: null,
            uploadUrl: '@Url.Action("Avatar", "MyProfile")',
            redirectUrl: '@Url.Action("Index", "Home")',
        };

        // Configuration object for Cropper
        const cropperConfig = {
            aspectRatio: 1,
            viewMode: 1,
            autoCropArea: 1,
        };

        // Handle file input change
        function handleFileChange(event) {
            const image = document.getElementById('preview');
            const file = event.target.files[0];

            if (!file) return;

            const objectURL = URL.createObjectURL(file);
            image.src = objectURL;

            image.addEventListener('load', () => {
                if (state.cropper) {
                    state.cropper.destroy();
                }

                // Initialize the cropper with config object
                state.cropper = new Cropper(image, cropperConfig);
            }, { once: true }); // Use once: true to prevent memory leaks
        }

        // Handle crop and upload
        async function handleCropAndUpload() {
            if (!state.cropper) {
                alert('Please select an image to crop.');
                return;
            }

            try {
                const canvas = state.cropper.getCroppedCanvas({
                    width: 1000,
                    height: 1000
                });

                if (!canvas) {
                    throw new Error('Failed to generate canvas');
                }

                const blob = await new Promise((resolve) => {
                    canvas.toBlob(resolve, 'image/png', 0.9);
                });

                if (!blob) {
                    throw new Error('Failed to generate image blob');
                }

                const formData = new FormData();
                formData.append('avatar', new File([blob], 'avatar.png', {
                    type: 'image/png'
                }));

                const response = await fetch(state.uploadUrl, {
                    method: 'POST',
                    body: formData
                });

                const data = await response.json();

                if (data.success) {
                    window.location.href = state.redirectUrl;
                } else {
                    throw new Error(data.error || 'Upload failed');
                }
            } catch (error) {
                console.error('Upload error:', error);
                alert(`An error occurred: ${error.message}`);
            }
        }

        // Add event listeners
        document.getElementById('avatar').addEventListener('change', handleFileChange);
        document.getElementById('cropButton').addEventListener('click', handleCropAndUpload);
    </script>
}

I get this error in an alert:

Error uploading avatar: Value cannot be null. (Parameter ‘path3’)

I also get this error in browser tools

The Content Security Policy (CSP) prevents cross-site scripting
attacks by blocking inline execution of scripts and style sheets.

To solve this, move all inline scripts (e.g. onclick=[JS code]) and
styles into external files.

⚠️ Allowing inline execution comes at the risk of script injection via
injection of HTML script elements. If you absolutely must, you can
allow inline script and styles by:

adding unsafe-inline as a source to the CSP header adding the hash or
nonce of the inline script to your CSP header. 1 directive
Directive Element Source location Status
script-src-elem Avatar:119 blocked Learn more: Content Security
Policy – Inline Code

I’ve asked both Claude and Chat GPT 40 and o1. They suggested that I put this meta tag in but it didn’t help

and doing that that may cause security issues anyway.

I have a feeling it’s related to the unsafe CSP header but a google search revealed nothing.

How can I get this working on the server?

Thanks