[bugfix] check remote status permissibility (#2703)

* add more stringent checks for remote status permissibility

* add check for inreplyto of a remote status being a boost

* do not permit inReplyTo boost wrapper statuses

* change comment wording

* fix calls to NewFederator()

* add code comments for NotPermitted() and SetNotPermitted()

* improve comment

* check that existing != nil before attempting delete

* ensure replying account isn't suspended

* use a debug log instead of info. check for boost using ID

* shorten log string length. make info level

* add note that replying to boost wrapper status shouldn't be able to happen anyways

* update to use onFail() function
This commit is contained in:
kim 2024-03-04 12:30:12 +00:00 committed by GitHub
commit d85727e184
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 154 additions and 16 deletions

View file

@ -22,6 +22,7 @@ import (
"sync"
"time"
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/transport"
@ -72,6 +73,7 @@ type Dereferencer struct {
converter *typeutils.Converter
transportController transport.Controller
mediaManager *media.Manager
visibility *visibility.Filter
// all protected by State{}.FedLocks.
derefAvatars map[string]*media.ProcessingMedia
@ -87,6 +89,7 @@ func NewDereferencer(
state *state.State,
converter *typeutils.Converter,
transportController transport.Controller,
visFilter *visibility.Filter,
mediaManager *media.Manager,
) Dereferencer {
return Dereferencer{
@ -94,6 +97,7 @@ func NewDereferencer(
converter: converter,
transportController: transportController,
mediaManager: mediaManager,
visibility: visFilter,
derefAvatars: make(map[string]*media.ProcessingMedia),
derefHeaders: make(map[string]*media.ProcessingMedia),
derefEmojis: make(map[string]*media.ProcessingEmoji),

View file

@ -77,8 +77,10 @@ func (suite *DereferencerStandardTestSuite) SetupTest() {
suite.storage = testrig.NewInMemoryStorage()
suite.state.DB = suite.db
suite.state.Storage = suite.storage
visFilter := visibility.NewFilter(&suite.state)
media := testrig.NewTestMediaManager(&suite.state)
suite.dereferencer = dereferencing.NewDereferencer(&suite.state, converter, testrig.NewTestTransportController(&suite.state, suite.client), media)
suite.dereferencer = dereferencing.NewDereferencer(&suite.state, converter, testrig.NewTestTransportController(&suite.state, suite.client), visFilter, media)
testrig.StandardDBSetup(suite.db, nil)
}

View file

@ -503,9 +503,16 @@ func (d *Dereferencer) enrichStatus(
latestStatus.FetchedAt = time.Now()
latestStatus.Local = status.Local
// Ensure the status' poll remains consistent, else reset the poll.
if err := d.fetchStatusPoll(ctx, status, latestStatus); err != nil {
return nil, nil, gtserror.Newf("error populating poll for status %s: %w", uri, err)
// Check if this is a permitted status we should accept.
permit, err := d.isPermittedStatus(ctx, status, latestStatus)
if err != nil {
return nil, nil, gtserror.Newf("error checking permissibility for status %s: %w", uri, err)
}
if !permit {
// Return a checkable error type that can be ignored.
err := gtserror.Newf("dropping unpermitted status: %s", uri)
return nil, nil, gtserror.SetNotPermitted(err)
}
// Ensure the status' mentions are populated, and pass in existing to check for changes.
@ -513,6 +520,11 @@ func (d *Dereferencer) enrichStatus(
return nil, nil, gtserror.Newf("error populating mentions for status %s: %w", uri, err)
}
// Ensure the status' poll remains consistent, else reset the poll.
if err := d.fetchStatusPoll(ctx, status, latestStatus); err != nil {
return nil, nil, gtserror.Newf("error populating poll for status %s: %w", uri, err)
}
// Now that we know who this status replies to (handled by ASStatusToStatus)
// and who it mentions, we can add a ThreadID to it if necessary.
if err := d.threadStatus(ctx, latestStatus); err != nil {
@ -550,6 +562,84 @@ func (d *Dereferencer) enrichStatus(
return latestStatus, apubStatus, nil
}
// isPermittedStatus returns whether the given status
// is permitted to be stored on this instance, checking
// whether the author is suspended, and passes visibility
// checks against status being replied-to (if any).
func (d *Dereferencer) isPermittedStatus(
ctx context.Context,
existing *gtsmodel.Status,
status *gtsmodel.Status,
) (
permitted bool, // is permitted?
err error,
) {
// our failure condition handling
// at the end of this function for
// the case of permission = false.
onFail := func() (bool, error) {
if existing != nil {
log.Infof(ctx, "deleting unpermitted: %s", existing.URI)
// Delete existing status from database as it's no longer permitted.
if err := d.state.DB.DeleteStatusByID(ctx, existing.ID); err != nil {
log.Errorf(ctx, "error deleting %s after permissivity fail: %v", existing.URI, err)
}
}
return false, nil
}
if !status.Account.SuspendedAt.IsZero() {
// The status author is suspended,
// this shouldn't have reached here
// but it's a fast check anyways.
return onFail()
}
if status.InReplyToURI == "" {
// This status isn't in
// reply to anything!
return true, nil
}
if status.InReplyTo == nil {
// If no inReplyTo has been set,
// we return here for now as we
// can't perform further checks.
//
// Worst case we allow something
// through, and later on during
// refetch it will get deleted.
return true, nil
}
if status.InReplyTo.BoostOfID != "" {
// We do not permit replies to
// boost wrapper statuses. (this
// shouldn't be able to happen).
return onFail()
}
// Check visibility of inReplyTo to status author.
permitted, err = d.visibility.StatusVisible(ctx,
status.Account,
status.InReplyTo,
)
if err != nil {
return false, gtserror.Newf("error checking in-reply-to visibility: %w", err)
}
if permitted &&
*status.InReplyTo.Replyable {
// This status is visible AND
// replyable, in this economy?!
return true, nil
}
return onFail()
}
// populateMentionTarget tries to populate the given
// mention with the correct TargetAccount and (if not
// yet set) TargetAccountURI, returning the populated