CVE-2022-2756 (published 2022-08-10T16:15Z)

Server-Side Request Forgery (SSRF) in GitHub repository kareadita/kavita prior to 0.5.4.1.

CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-918
cwe_name: Server-Side Request Forgery (SSRF)
description: 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.
extended_description: By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly. The server can be used as a proxy to conduct port scanning of hosts in internal networks, use other URLs such as that can access documents on the system (using file://), or use other protocols such as gopher:// or tftp://, which may provide greater control over the contents of requests.
         

Fix information:
repo_url: https://github.com/kareadita/kavita
commit_details: repo_url: https://github.com/kareadita/kavita
author: Joseph Milazzo
author_date: 2022-08-08 15:22:38-05:00
msg: 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
repository: repo_url: https://github.com/kareadita/kavita
repo_name: Kareadita/Kavita
description: Kavita is a fast, feature rich, cross platform reading server. Built with the goal of being a full solution for all your reading needs. Setup your own server and share your reading collection with your friends and family.
repo_language: C#
owner: kareadita
file_changes: filename: AccountController.cs
change_type: ModificationType.MODIFY
diff: @@ -70,13 +70,21 @@ namespace API.Controllers
         /// </summary>
         /// <param name="resetPasswordDto"></param>
         /// <returns></returns>
+        [AllowAnonymous]
         [HttpPost("reset-password")]
         public async Task<ActionResult> UpdatePassword(ResetPasswordDto resetPasswordDto)
         {
+            // TODO: Log this request to Audit Table
             _logger.LogInformation("{UserName} is changing {ResetUser}'s password", User.GetUsername(), resetPasswordDto.UserName);
-            var user = await _userManager.Users.SingleAsync(x => x.UserName == resetPasswordDto.UserName);
 
-            if (resetPasswordDto.UserName != User.GetUsername() && !(User.IsInRole(PolicyConstants.AdminRole) || User.IsInRole(PolicyConstants.ChangePasswordRole)))
+            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))
                 return Unauthorized("You are not permitted to this operation.");
 
             var errors = await _accountService.ChangeUserPassword(user, resetPasswordDto.Password);
@@ -94,6 +102,7 @@ namespace API.Controllers
         /// </summary>
         /// <param name="registerDto"></param>
         /// <returns></returns>
+        [AllowAnonymous]
         [HttpPost("register")]
         public async Task<ActionResult<UserDto>> RegisterFirstUser(RegisterDto registerDto)
         {
@@ -155,6 +164,7 @@ namespace API.Controllers
         /// </summary>
         /// <param name="loginDto"></param>
         /// <returns></returns>
+        [AllowAnonymous]
         [HttpPost("login")]
         public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
         {
@@ -173,14 +183,14 @@ namespace API.Controllers
                     "You are missing an email on your account. Please wait while we migrate your account.");
             }
 
-            if (!validPassword)
+            var result = await _signInManager
+                .CheckPasswordSignInAsync(user, loginDto.Password, true);
+
+            if (result.IsLockedOut)
             {
-                return Unauthorized("Your credentials are not correct");
+                return Unauthorized("You've been locked out from too many authorization attempts. Please wait 10 minutes.");
             }
 
-            var result = await _signInManager
-                .CheckPasswordSignInAsync(user, loginDto.Password, false);
-
             if (!result.Succeeded)
             {
                 return Unauthorized(result.IsNotAllowed ? "You must confirm your email first" : "Your credentials are not correct.");
@@ -212,6 +222,7 @@ namespace API.Controllers
         /// </summary>
         /// <param name="tokenRequestDto"></param>
         /// <returns></returns>
+        [AllowAnonymous]
         [HttpPost("refresh-token")]
         public async Task<ActionResult<TokenRequestDto>> RefreshToken([FromBody] TokenRequestDto tokenRequestDto)
         {
@@ -462,6 +473,7 @@ namespace API.Controllers
             return BadRequest("There was an error setting up your account. Please check the logs");
         }
 
+        [AllowAnonymous]
         [HttpPost("confirm-email")]
         public async Task<ActionResult<UserDto>> ConfirmEmail(ConfirmEmailDto dto)
         {

programming_language: C#
filename: AdminController.cs
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,6 @@
 ﻿using System.Threading.Tasks;
 using API.Entities;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Identity;
 using Microsoft.AspNetCore.Mvc;
 
@@ -18,6 +19,7 @@ namespace API.Controllers
         /// Checks if an admin exists on the system. This is essentially a check to validate if the system has been setup.
         /// </summary>
         /// <returns></returns>
+        [AllowAnonymous]
         [HttpGet("exists")]
         public async Task<ActionResult<bool>> AdminExists()
         {

programming_language: C#
filename: BaseApiController.cs
change_type: ModificationType.MODIFY
diff: @@ -1,10 +1,12 @@
-﻿using Microsoft.AspNetCore.Mvc;
+﻿using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
 
 namespace API.Controllers
 {
     [ApiController]
     [Route("api/[controller]")]
+    [Authorize]
     public class BaseApiController : ControllerBase
     {
     }
-}
\ No newline at end of file
+}

programming_language: C#
filename: FallbackController.cs
change_type: ModificationType.MODIFY
diff: @@ -1,24 +1,26 @@
 ﻿using System.IO;
 using API.Services;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 
-namespace API.Controllers
+namespace API.Controllers;
+
+[AllowAnonymous]
+public class FallbackController : Controller
 {
-    public class FallbackController : Controller
-    {
-        // ReSharper disable once S4487
-        // ReSharper disable once NotAccessedField.Local
-        private readonly ITaskScheduler _taskScheduler;
+    // 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 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");
-        }
+    public ActionResult Index()
+    {
+        return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML");
     }
 }
+

programming_language: C#
filename: ImageController.cs
change_type: ModificationType.MODIFY
diff: @@ -137,6 +137,8 @@ namespace API.Controllers
         [HttpGet("cover-upload")]
         public ActionResult GetCoverUploadImage(string filename)
         {
+            if (filename.Contains("..")) return BadRequest("Invalid Filename");
+
             var path = Path.Join(_directoryService.TempDirectory, filename);
             if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"File does not exist");
             var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");

programming_language: C#
filename: OPDSController.cs
change_type: ModificationType.MODIFY
diff: @@ -17,10 +17,12 @@ using API.Extensions;
 using API.Helpers;
 using API.Services;
 using Kavita.Common;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 
 namespace API.Controllers;
 
+[AllowAnonymous]
 public class OpdsController : BaseApiController
 {
     private readonly IUnitOfWork _unitOfWork;

programming_language: C#
filename: ReaderController.cs
change_type: ModificationType.MODIFY
diff: @@ -57,6 +57,11 @@ namespace API.Controllers
             var chapter = await _cacheService.Ensure(chapterId);
             if (chapter == null) return BadRequest("There was an issue finding pdf file for reading");
 
+            // 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");
+
             try
             {
                 var path = _cacheService.GetCachedFile(chapter);

programming_language: C#
filename: ReadingListController.cs
change_type: ModificationType.MODIFY
diff: @@ -3,15 +3,18 @@ using System.Linq;
 using System.Threading.Tasks;
 using API.Comparators;
 using API.Data;
+using API.Data.Repositories;
 using API.DTOs.ReadingLists;
 using API.Entities;
 using API.Extensions;
 using API.Helpers;
 using API.SignalR;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 
 namespace API.Controllers
 {
+    [Authorize]
     public class ReadingListController : BaseApiController
     {
         private readonly IUnitOfWork _unitOfWork;
@@ -73,8 +76,18 @@ namespace API.Controllers
             var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
             var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
             return Ok(items);
+        }
+
+        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 Ok(await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList()));
+            return user;
         }
 
         /// <summary>
@@ -86,6 +99,11 @@ namespace API.Controllers
         public async Task<ActionResult> UpdateListItemPosition(UpdateReadingListPosition dto)
         {
             // Make sure UI buffers events
+            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 items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList();
             var item = items.Find(r => r.Id == dto.ReadingListItemId);
             items.Remove(item);
@@ -112,10 +130,15 @@ namespace API.Controllers
         [HttpPost("delete-item")]
         public async Task<ActionResult> DeleteListItem(UpdateReadingListPosition dto)
         {
+            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 readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
             readingList.Items = readingList.Items.Where(r => r.Id != dto.ReadingListItemId).ToList();
 
-
             var index = 0;
             foreach (var readingListItem in readingList.Items)
             {
@@ -141,9 +164,14 @@ namespace API.Controllers
         [HttpPost("remove-read")]
         public async Task<ActionResult> DeleteReadFromList([FromQuery] int readingListId)
         {
-            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 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());
 
             // Collect all Ids to remove
             var itemIdsToRemove = items.Where(item => item.PagesRead == item.PagesTotal).Select(item => item.Id);
@@ -176,15 +204,13 @@ namespace API.Controllers
         [HttpDelete]
         public async Task<ActionResult> DeleteList([FromQuery] int readingListId)
         {
-            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)
+            var user = await UserHasReadingListAccess(readingListId);
+            if (user == null)
             {
-                return BadRequest("User is not associated with this reading list");
+                return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
             }
 
-            readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId);
+            var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId);
 
             user.ReadingLists.Remove(readingList);
 
@@ -213,13 +239,14 @@ namespace API.Controllers
                 return BadRequest("A list of this name already exists");
             }
 
-            user.ReadingLists.Add(DbFactory.ReadingList(dto.Title, string.Empty, false));
+            var readingList = DbFactory.ReadingList(dto.Title, string.Empty, false);
+            user.ReadingLists.Add(readingList);
 
             if (!_unitOfWork.HasChanges()) return BadRequest("There was a problem creating list");
 
             await _unitOfWork.CommitAsync();
 
-            return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(dto.Title));
+            return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(user.Id, dto.Title));
         }
 
         /// <summary>
@@ -233,7 +260,11 @@ namespace API.Controllers
             var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
             if (readingList == null) return BadRequest("List does not exist");
 
-
+            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");
+            }
 
             if (!string.IsNullOrEmpty(dto.Title))
             {
@@ -277,7 +308,12 @@ namespace API.Controllers
         [HttpPost("update-by-series")]
         public async Task<ActionResult> UpdateListBySeries(UpdateReadingListBySeriesDto dto)
         {
-            var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
+            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 readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
             if (readingList == null) return BadRequest("Reading List does not exist");
             var chapterIdsForSeries =
@@ -314,7 +350,11 @@ namespace API.Controllers
         [HttpPost("update-by-multiple")]
         public async Task<ActionResult> UpdateListByMultiple(UpdateReadingListByMultipleDto dto)
         {
-            var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
+            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 readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
             if (readingList == null) return BadRequest("Reading List does not exist");
 
@@ -354,7 +394,11 @@ namespace API.Controllers
         [HttpPost("update-by-multiple-series")]
         public async Task<ActionResult> UpdateListByMultipleSeries(UpdateReadingListByMultipleSeriesDto dto)
         {
-            var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
+            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 readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
             if (readingList == null) return BadRequest("Reading List does not exist");
 
@@ -388,9 +432,14 @@ namespace API.Controllers
         [HttpPost("update-by-volume")]
         public async Task<ActionResult> UpdateListByVolume(UpdateReadingListByVolumeDto dto)
         {
-            var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
+            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 readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
             if (readingList == null) return BadRequest("Reading List does not exist");
+
             var chapterIdsForVolume =
                 (await _unitOfWork.ChapterRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList();
 
@@ -419,7 +468,11 @@ namespace API.Controllers
         [HttpPost("update-by-chapter")]
         public async Task<ActionResult> UpdateListByChapter(UpdateReadingListByChapterDto dto)
         {
-            var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
+            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 readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
             if (readingList == null) return BadRequest("Reading List does not exist");
 

programming_language: C#
filename: ThemeController.cs
change_type: ModificationType.MODIFY
diff: @@ -24,6 +24,7 @@ public class ThemeController : BaseApiController
         _taskScheduler = taskScheduler;
     }
 
+    [AllowAnonymous]
     [HttpGet]
     public async Task<ActionResult<IEnumerable<SiteThemeDto>>> GetThemes()
     {

programming_language: C#
filename: UploadController.cs
change_type: ModificationType.MODIFY
diff: @@ -59,6 +59,8 @@ namespace API.Controllers
                 if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
                     return BadRequest($"Could not download file");
 
+                if (!await _imageService.IsImage(path)) return BadRequest("Url does not return a valid image");
+
                 return $"coverupload_{dateString}.{format}";
             }
             catch (FlurlHttpException ex)

programming_language: C#
filename: ReadingListRepository.cs
change_type: ModificationType.MODIFY
diff: @@ -17,7 +17,7 @@ public interface IReadingListRepository
     Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId);
     Task<ReadingListDto> GetReadingListDtoByIdAsync(int readingListId, int userId);
     Task<IEnumerable<ReadingListItemDto>> AddReadingProgressModifiers(int userId, IList<ReadingListItemDto> items);
-    Task<ReadingListDto> GetReadingListDtoByTitleAsync(string title);
+    Task<ReadingListDto> GetReadingListDtoByTitleAsync(int userId, string title);
     Task<IEnumerable<ReadingListItem>> GetReadingListItemsByIdAsync(int readingListId);
 
     Task<IEnumerable<ReadingListDto>> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId,
@@ -211,10 +211,10 @@ public class ReadingListRepository : IReadingListRepository
         return items;
     }
 
-    public async Task<ReadingListDto> GetReadingListDtoByTitleAsync(string title)
+    public async Task<ReadingListDto> GetReadingListDtoByTitleAsync(int userId, string title)
     {
         return await _context.ReadingList
-            .Where(r => r.Title.Equals(title))
+            .Where(r => r.Title.Equals(title) && r.AppUserId == userId)
             .ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
             .SingleOrDefaultAsync();
     }

programming_language: C#
filename: IdentityServiceExtensions.cs
change_type: ModificationType.MODIFY
diff: @@ -1,4 +1,5 @@
-﻿using System.Text;
+﻿using System;
+using System.Text;
 using System.Threading.Tasks;
 using API.Constants;
 using API.Data;
@@ -32,6 +33,11 @@ namespace API.Extensions
                     opt.Password.RequiredLength = 6;
 
                     opt.SignIn.RequireConfirmedEmail = true;
+
+                    opt.Lockout.AllowedForNewUsers = true;
+                    opt.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
+                    opt.Lockout.MaxFailedAccessAttempts = 5;
+
                 })
                 .AddTokenProvider<DataProtectorTokenProvider<AppUser>>(TokenOptions.DefaultProvider)
                 .AddRoles<AppRole>()

programming_language: C#
filename: ImageService.cs
change_type: ModificationType.MODIFY
diff: @@ -27,6 +27,8 @@ public interface IImageService
     /// <param name="filePath">Full path to the image to convert</param>
     /// <returns>File of written webp image</returns>
     Task<string> ConvertToWebP(string filePath, string outputPath);
+
+    Task<bool> IsImage(string filePath);
 }
 
 public class ImageService : IImageService
@@ -115,6 +117,23 @@ public class ImageService : IImageService
         return outputFile;
     }
 
+    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;
+    }
+
 
     /// <inheritdoc />
     public string CreateThumbnailFromBase64(string encodedImage, string fileName)

programming_language: C#
filename: Kavita.Common.csproj
change_type: ModificationType.MODIFY
diff: @@ -4,7 +4,7 @@
         <TargetFramework>net6.0</TargetFramework>
         <Company>kavitareader.com</Company>
         <Product>Kavita</Product>
-        <AssemblyVersion>0.5.4.0</AssemblyVersion>
+        <AssemblyVersion>0.5.4.1</AssemblyVersion>
         <NeutralLanguage>en</NeutralLanguage>
     </PropertyGroup>
 

programming_language: C#
filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -21,12 +21,12 @@ your reading collection with your friends and family!
 - [x] Serve up Manga/Webtoons/Comics (cbr, cbz, zip/rar, 7zip, raw images) and Books (epub, pdf)
 - [x] First class responsive readers that work great on any device (phone, tablet, desktop)
 - [x] Dark mode and customizable theming support
-- [ ] Provide hooks into metadata providers to fetch metadata for Comics, Manga, and Books
+- [ ] Provide a plugin system to allow external metadata integration and scrobbling for read status, ratings, and reviews
 - [x] Metadata should allow for collections, want to read integration from 3rd party services, genres.
 - [x] Ability to manage users, access, and ratings
-- [ ] Ability to sync ratings and reviews to external services
 - [x] Fully Accessible with active accessibility audits
 - [x] Dedicated webtoon reading mode
+- [ ] Full localization support
 - [ ] And so much [more...](https://github.com/Kareadita/Kavita/projects)
 
 ## Support
@@ -93,6 +93,9 @@ Thank you to [<img src="/Logo/jetbrains.svg" alt="" width="32"> JetBrains](http:
 ## Palace-Designs
 We would like to extend a big thank you to [<img src="/Logo/hosting-sponsor.png" alt="" width="128">](https://www.palace-designs.com/) who hosts our infrastructure pro-bono.
 
+## 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.
 
 ### License
 

programming_language: Markdown
filename: SECURITY.md
change_type: ModificationType.ADD
diff: @@ -0,0 +1,10 @@
+# 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).

programming_language: Markdown