CVE-2022-2756 (C#, published 2022-08-10T16:15Z)
Description: value: Server-Side Request Forgery (SSRF) in GitHub repository kareadita/kavita prior to 0.5.4.1.
Severity: nan (CVSS v3 base score: 6.5)
Weakness: CWE-918 \u2014 Server-Side Request Forgery (SSRF)
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
Repository: https://github.com/kareadita/kavita
Files changed:
API/Controllers/AccountController.cs
API/Controllers/AdminController.cs
API/Controllers/BaseApiController.cs
API/Controllers/FallbackController.cs
API/Controllers/ImageController.cs
API/Controllers/OPDSController.cs
API/Controllers/ReaderController.cs
API/Controllers/ReadingListController.cs
API/Controllers/ThemeController.cs
API/Controllers/UploadController.cs
API/Data/Repositories/ReadingListRepository.cs
API/Extensions/IdentityServiceExtensions.cs
API/Services/ImageService.cs
Kavita.Common/Kavita.Common.csproj
README.md
SECURITY.md
Commit message: v0.5.4.1 - Security Hotfix (#1414)

* Updated our security file with our Huntr.

* Fixed a security vulnerability where through the API, an unauthorized user could delete/modify reading lists that did not belong to them.

Fixed a bug where when creating a reading list with the name of another users, the API would throw an exception (but reading list would still get created)

* Fixed a security vulnerability where through the API, an unauthorized user could delete/modify reading lists that did not belong to them.

Fixed a bug where when creating a reading list with the name of another users, the API would throw an exception (but reading list would still get created)

* Ensure all reading list apis are authorized

* Ensured all APIs require authentication, except those that explicitly don't. All APIs are default requiring Authentication.

Fixed a security vulnerability which would allow a user to take over an admin account.

* Fixed a bug where cover-upload would accept filenames that were not expected.

* Explicitly check that a user has access to the pdf file before we serve it back.

* Enabled lock out when invalid user auth occurs. After 5 invalid auths, the user account will be locked out for 10 mins.

* Version bump for hotfix release

Vulnerable code:
            var user = await _userManager.Users.SingleAsync(x => x.UserName == resetPasswordDto.UserName);
            if (resetPasswordDto.UserName != User.GetUsername() && !(User.IsInRole(PolicyConstants.AdminRole) || User.IsInRole(PolicyConstants.ChangePasswordRole)))
            if (!validPassword)
                return Unauthorized("Your credentials are not correct");
            var result = await _signInManager
                .CheckPasswordSignInAsync(user, loginDto.Password, false);

﻿using Microsoft.AspNetCore.Mvc;
}
namespace API.Controllers
    public class FallbackController : Controller
    {
        // ReSharper disable once S4487
        // ReSharper disable once NotAccessedField.Local
        private readonly ITaskScheduler _taskScheduler;
        public FallbackController(ITaskScheduler taskScheduler)
        {
            // This is used to load TaskScheduler on startup without having to navigate to a Controller that uses.
            _taskScheduler = taskScheduler;
        }
        public ActionResult Index()
        {
            return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML");
        }
            //return Ok(await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList()));

            var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
            var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
            items = await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList());
            var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
            var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
            var readingList = user.ReadingLists.SingleOrDefault(r => r.Id == readingListId);
            if (readingList == null && !isAdmin)
                return BadRequest("User is not associated with this reading list");
            readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId);
            user.ReadingLists.Add(DbFactory.ReadingList(dto.Title, string.Empty, false));
            return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(dto.Title));

            var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
            var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
            var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
            var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
            var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
    Task<ReadingListDto> GetReadingListDtoByTitleAsync(string title);
    public async Task<ReadingListDto> GetReadingListDtoByTitleAsync(string title)
            .Where(r => r.Title.Equals(title))
﻿using System.Text;
        <AssemblyVersion>0.5.4.0</AssemblyVersion>
- [ ] Provide hooks into metadata providers to fetch metadata for Comics, Manga, and Books
- [ ] Ability to sync ratings and reviews to external services

Fixed code:
        [AllowAnonymous]
            // TODO: Log this request to Audit Table
            var user = await _userManager.Users.SingleOrDefaultAsync(x => x.UserName == resetPasswordDto.UserName);
            if (user == null) return Ok(); // Don't report BadRequest as that would allow brute forcing to find accounts on system


            if (resetPasswordDto.UserName == User.GetUsername() && !(User.IsInRole(PolicyConstants.ChangePasswordRole) || User.IsInRole(PolicyConstants.AdminRole)))
                return Unauthorized("You are not permitted to this operation.");

            if (resetPasswordDto.UserName != User.GetUsername() && !User.IsInRole(PolicyConstants.AdminRole))
        [AllowAnonymous]
        [AllowAnonymous]
            var result = await _signInManager
                .CheckPasswordSignInAsync(user, loginDto.Password, true);

            if (result.IsLockedOut)
                return Unauthorized("You've been locked out from too many authorization attempts. Please wait 10 minutes.");
        [AllowAnonymous]
        [AllowAnonymous]
using Microsoft.AspNetCore.Authorization;
        [AllowAnonymous]
﻿using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
    [Authorize]
}
using Microsoft.AspNetCore.Authorization;
namespace API.Controllers;

[AllowAnonymous]
public class FallbackController : Controller
    // ReSharper disable once S4487
    // ReSharper disable once NotAccessedField.Local
    private readonly ITaskScheduler _taskScheduler;
    public FallbackController(ITaskScheduler taskScheduler)
    {
        // This is used to load TaskScheduler on startup without having to navigate to a Controller that uses.
        _taskScheduler = taskScheduler;
    }
    public ActionResult Index()
    {
        return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML");

            if (filename.Contains("..")) return BadRequest("Invalid Filename");

using Microsoft.AspNetCore.Authorization;
[AllowAnonymous]
            // Validate the user has access to the PDF
            var series = await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapter.Id,
                await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()));
            if (series == null) return BadRequest("Invalid Access");

using API.Data.Repositories;
using Microsoft.AspNetCore.Authorization;
    [Authorize]
        }

        private async Task<AppUser?> UserHasReadingListAccess(int readingListId)
        {
            var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(),
                AppUserIncludes.ReadingLists);
            if (user.ReadingLists.SingleOrDefault(rl => rl.Id == readingListId) == null && !await _unitOfWork.UserRepository.IsUserAdminAsync(user))
            {
                return null;
            }
            return user;
            var user = await UserHasReadingListAccess(dto.ReadingListId);
            if (user == null)
            {
                return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
            }
            var user = await UserHasReadingListAccess(dto.ReadingListId);
            if (user == null)
            {
                return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
            }

            var user = await UserHasReadingListAccess(readingListId);
            if (user == null)
            {
                return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
            }

            var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, user.Id);
            items = await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(user.Id, items.ToList());
            var user = await UserHasReadingListAccess(readingListId);
            if (user == null)
                return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
            var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId);
            var readingList = DbFactory.ReadingList(dto.Title, string.Empty, false);
            user.ReadingLists.Add(readingList);
            return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(user.Id, dto.Title));
            var user = await UserHasReadingListAccess(readingList.Id);
            if (user == null)
            {
                return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
            }
            var user = await UserHasReadingListAccess(dto.ReadingListId);
            if (user == null)
            {
                return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
            }

            var user = await UserHasReadingListAccess(dto.ReadingListId);
            if (user == null)
            {
                return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
            }
            var user = await UserHasReadingListAccess(dto.ReadingListId);
            if (user == null)
            {
                return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
            }
            var user = await UserHasReadingListAccess(dto.ReadingListId);
            if (user == null)
            {
                return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
            }

            var user = await UserHasReadingListAccess(dto.ReadingListId);
            if (user == null)
            {
                return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
            }
    [AllowAnonymous]
                if (!await _imageService.IsImage(path)) return BadRequest("Url does not return a valid image");

    Task<ReadingListDto> GetReadingListDtoByTitleAsync(int userId, string title);
    public async Task<ReadingListDto> GetReadingListDtoByTitleAsync(int userId, string title)
            .Where(r => r.Title.Equals(title) && r.AppUserId == userId)
﻿using System;
using System.Text;

                    opt.Lockout.AllowedForNewUsers = true;
                    opt.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
                    opt.Lockout.MaxFailedAccessAttempts = 5;


    Task<bool> IsImage(string filePath);
    public async Task<bool> IsImage(string filePath)
    {
        try
        {
            var info = await SixLabors.ImageSharp.Image.IdentifyAsync(filePath);
            if (info == null) return false;

            return true;
        }
        catch (Exception ex)
        {
            /* Swallow Exception */
        }

        return false;
    }

        <AssemblyVersion>0.5.4.1</AssemblyVersion>
- [ ] Provide a plugin system to allow external metadata integration and scrobbling for read status, ratings, and reviews
- [ ] Full localization support
## Huntr
We would like to extend a big thank you to [Huntr](https://huntr.dev/repos/kareadita/kavita) who has worked with Kavita in reporting security vulnerabilities. If you are interested in 
being paid to help secure Kavita, please give them a try.
# Security Policy

## Supported Versions

Security is maintained on latest stable version only. 

## Reporting a Vulnerability


Please reach out to majora2007 via our Discord or you can (and should) report your vulnerability via [Huntr](https://huntr.dev/repos/kareadita/kavita).