From 863c9ca9c97bd5a7ec827ac398141c14973316ad Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Wed, 23 Oct 2024 20:40:31 +0200 Subject: [PATCH] [backend/federation] Improve actor & note validation (ISH-547) --- .../Federation/ActivityPub/UserResolver.cs | 4 ++-- .../ActivityStreams/Types/ASActor.cs | 22 +++++++++++++++++-- .../Core/Services/NoteService.cs | 2 ++ .../Core/Services/UserService.cs | 6 ++--- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/UserResolver.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/UserResolver.cs index 585b5b24..5acc08df 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/UserResolver.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/UserResolver.cs @@ -74,7 +74,7 @@ public class UserResolver( } logger.LogDebug("Actor ID matches query, performing reverse discovery..."); - actor.Normalize(query); + actor.NormalizeAndValidate(query); var domain = new Uri(actor.Id).Host; var username = actor.Username!; return await WebFingerAsync(actor.WebfingerAddress ?? $"acct:{username}@{domain}", false, actor.Id); @@ -124,7 +124,7 @@ public class UserResolver( throw new Exception("Reverse discovery fallback failed: uri mismatch"); logger.LogDebug("Actor ID matches apUri, performing reverse discovery..."); - actor.Normalize(apUri); + actor.NormalizeAndValidate(apUri); var domain = new Uri(actor.Id).Host; var username = new Uri(actor.Username!).Host; return await WebFingerAsync(actor.WebfingerAddress ?? $"acct:{username}@{domain}", false, diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActor.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActor.cs index 51df8d8c..44072b7a 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActor.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActor.cs @@ -1,6 +1,7 @@ using System.Text.RegularExpressions; using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Extensions; +using Iceshrimp.Backend.Core.Middleware; using J = Newtonsoft.Json.JsonPropertyAttribute; using JC = Newtonsoft.Json.JsonConverterAttribute; using JI = Newtonsoft.Json.JsonIgnoreAttribute; @@ -141,7 +142,7 @@ public class ASActor : ASObjectWithId [JI] public bool IsBot => Type == $"{Constants.ActivityStreamsNs}#Service"; - public void Normalize(string uri) + public void NormalizeAndValidate(string uri) { if (Type == null || !ActorTypes.Contains(Type)) throw new Exception("Actor is of invalid type"); @@ -154,9 +155,26 @@ public class ASActor : ASObjectWithId !Regex.IsMatch(Username, @"^\w([\w-.]*\w)?$")) throw new Exception("Actor username is invalid"); + var uriHost = new Uri(uri).Host; + var publicKeyId = PublicKey?.Id ?? throw new Exception("Invalid actor: missing PublicKey?.Id"); + var sharedInbox = SharedInbox?.Link ?? Endpoints?.SharedInbox?.Id; + if (Inbox?.Id == null) + throw GracefulException.UnprocessableEntity("Invalid actor: missing inbox"); if (new Uri(publicKeyId).Host != new Uri(uri).Host) - throw new Exception("Invalid actor: public key id / actor id host mismatch"); + throw GracefulException.UnprocessableEntity("Invalid actor: public key id / actor id host mismatch"); + if (new Uri(Inbox.Id).Host != uriHost) + throw GracefulException.UnprocessableEntity("Invalid actor: inbox host doesn't match id host"); + if (Outbox?.Id != null && new Uri(Outbox.Id).Host != uriHost) + throw GracefulException.UnprocessableEntity("Invalid actor: outbox doesn't match id host"); + if (sharedInbox != null && new Uri(sharedInbox).Host != uriHost) + throw GracefulException.UnprocessableEntity("Invalid actor: shared inbox host doesn't match id host"); + if (Followers?.Id != null && new Uri(Followers.Id).Host != uriHost) + throw GracefulException.UnprocessableEntity("Invalid actor: followers host doesn't match actor id host"); + if (Following?.Id != null && new Uri(Following.Id).Host != uriHost) + throw GracefulException.UnprocessableEntity("Invalid actor: following host doesn't match id host"); + if (Url?.Link != null && new Uri(Url.Link).Host != uriHost) + Url = null; DisplayName = DisplayName switch { diff --git a/Iceshrimp.Backend/Core/Services/NoteService.cs b/Iceshrimp.Backend/Core/Services/NoteService.cs index a278035b..314b9ab7 100644 --- a/Iceshrimp.Backend/Core/Services/NoteService.cs +++ b/Iceshrimp.Backend/Core/Services/NoteService.cs @@ -817,6 +817,8 @@ public class NoteService( throw GracefulException.UnprocessableEntity("Note.Id schema is invalid"); if (note.Url?.Link != null && !note.Url.Link.StartsWith("https://")) throw GracefulException.UnprocessableEntity("Note.Url schema is invalid"); + if (note.Url?.Link != null && new Uri(note.Id).IdnHost != new Uri(note.Url.Link).IdnHost) + note.Url = null; if (actor.IsSuspended) throw GracefulException.Forbidden("User is suspended"); if (await fedCtrlSvc.ShouldBlockAsync(note.Id, actor.Host)) diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs index 5e007509..ffd8f8b8 100644 --- a/Iceshrimp.Backend/Core/Services/UserService.cs +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -130,7 +130,7 @@ public class UserService( var actor = await fetchSvc.FetchActorAsync(uri); logger.LogDebug("Got actor: {url}", actor.Url); - actor.Normalize(uri); + actor.NormalizeAndValidate(uri); user = await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == actor.Username!.ToLowerInvariant() && p.Host == host); @@ -142,8 +142,6 @@ public class UserService( throw GracefulException.UnprocessableEntity("Uri doesn't match id of fetched actor"); if (actor.PublicKey?.Id == null || actor.PublicKey?.PublicKey == null) throw GracefulException.UnprocessableEntity("Actor has no valid public key"); - if (new Uri(actor.PublicKey.Id).Host != new Uri(actor.Id).Host) - throw GracefulException.UnprocessableEntity("Actor public key id host doesn't match actor id host"); var emoji = await emojiSvc.ProcessEmojiAsync(actor.Tags?.OfType().ToList(), host); @@ -278,7 +276,7 @@ public class UserService( logger.LogDebug("Updating user with uri {uri}", uri); actor ??= await fetchSvc.FetchActorAsync(user.Uri); - actor.Normalize(uri); + actor.NormalizeAndValidate(uri); user.UserProfile ??= await db.UserProfiles.FirstOrDefaultAsync(p => p.User == user); user.UserProfile ??= new UserProfile { User = user };