mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-22 11:46:40 +00:00
[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:
parent
f487fc5d4b
commit
d85727e184
|
@ -148,7 +148,7 @@ func(context.Context, time.Time) {
|
||||||
spamFilter := spam.NewFilter(&state)
|
spamFilter := spam.NewFilter(&state)
|
||||||
federatingDB := federatingdb.New(&state, typeConverter, visFilter, spamFilter)
|
federatingDB := federatingdb.New(&state, typeConverter, visFilter, spamFilter)
|
||||||
transportController := transport.NewController(&state, federatingDB, &federation.Clock{}, client)
|
transportController := transport.NewController(&state, federatingDB, &federation.Clock{}, client)
|
||||||
federator := federation.NewFederator(&state, federatingDB, transportController, typeConverter, mediaManager)
|
federator := federation.NewFederator(&state, federatingDB, transportController, typeConverter, visFilter, mediaManager)
|
||||||
|
|
||||||
// Decide whether to create a noop email
|
// Decide whether to create a noop email
|
||||||
// sender (won't send emails) or a real one.
|
// sender (won't send emails) or a real one.
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||||
|
@ -72,6 +73,7 @@ type Dereferencer struct {
|
||||||
converter *typeutils.Converter
|
converter *typeutils.Converter
|
||||||
transportController transport.Controller
|
transportController transport.Controller
|
||||||
mediaManager *media.Manager
|
mediaManager *media.Manager
|
||||||
|
visibility *visibility.Filter
|
||||||
|
|
||||||
// all protected by State{}.FedLocks.
|
// all protected by State{}.FedLocks.
|
||||||
derefAvatars map[string]*media.ProcessingMedia
|
derefAvatars map[string]*media.ProcessingMedia
|
||||||
|
@ -87,6 +89,7 @@ func NewDereferencer(
|
||||||
state *state.State,
|
state *state.State,
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
transportController transport.Controller,
|
transportController transport.Controller,
|
||||||
|
visFilter *visibility.Filter,
|
||||||
mediaManager *media.Manager,
|
mediaManager *media.Manager,
|
||||||
) Dereferencer {
|
) Dereferencer {
|
||||||
return Dereferencer{
|
return Dereferencer{
|
||||||
|
@ -94,6 +97,7 @@ func NewDereferencer(
|
||||||
converter: converter,
|
converter: converter,
|
||||||
transportController: transportController,
|
transportController: transportController,
|
||||||
mediaManager: mediaManager,
|
mediaManager: mediaManager,
|
||||||
|
visibility: visFilter,
|
||||||
derefAvatars: make(map[string]*media.ProcessingMedia),
|
derefAvatars: make(map[string]*media.ProcessingMedia),
|
||||||
derefHeaders: make(map[string]*media.ProcessingMedia),
|
derefHeaders: make(map[string]*media.ProcessingMedia),
|
||||||
derefEmojis: make(map[string]*media.ProcessingEmoji),
|
derefEmojis: make(map[string]*media.ProcessingEmoji),
|
||||||
|
|
|
@ -77,8 +77,10 @@ func (suite *DereferencerStandardTestSuite) SetupTest() {
|
||||||
suite.storage = testrig.NewInMemoryStorage()
|
suite.storage = testrig.NewInMemoryStorage()
|
||||||
suite.state.DB = suite.db
|
suite.state.DB = suite.db
|
||||||
suite.state.Storage = suite.storage
|
suite.state.Storage = suite.storage
|
||||||
|
|
||||||
|
visFilter := visibility.NewFilter(&suite.state)
|
||||||
media := testrig.NewTestMediaManager(&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)
|
testrig.StandardDBSetup(suite.db, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -503,9 +503,16 @@ func (d *Dereferencer) enrichStatus(
|
||||||
latestStatus.FetchedAt = time.Now()
|
latestStatus.FetchedAt = time.Now()
|
||||||
latestStatus.Local = status.Local
|
latestStatus.Local = status.Local
|
||||||
|
|
||||||
// Ensure the status' poll remains consistent, else reset the poll.
|
// Check if this is a permitted status we should accept.
|
||||||
if err := d.fetchStatusPoll(ctx, status, latestStatus); err != nil {
|
permit, err := d.isPermittedStatus(ctx, status, latestStatus)
|
||||||
return nil, nil, gtserror.Newf("error populating poll for status %s: %w", uri, err)
|
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.
|
// 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)
|
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)
|
// 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.
|
// and who it mentions, we can add a ThreadID to it if necessary.
|
||||||
if err := d.threadStatus(ctx, latestStatus); err != nil {
|
if err := d.threadStatus(ctx, latestStatus); err != nil {
|
||||||
|
@ -550,6 +562,84 @@ func (d *Dereferencer) enrichStatus(
|
||||||
return latestStatus, apubStatus, nil
|
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
|
// populateMentionTarget tries to populate the given
|
||||||
// mention with the correct TargetAccount and (if not
|
// mention with the correct TargetAccount and (if not
|
||||||
// yet set) TargetAccountURI, returning the populated
|
// yet set) TargetAccountURI, returning the populated
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
@ -60,14 +61,21 @@ func (suite *FederatingActorTestSuite) TestSendNoRemoteFollowers() {
|
||||||
tc := testrig.NewTestTransportController(&suite.state, httpClient)
|
tc := testrig.NewTestTransportController(&suite.state, httpClient)
|
||||||
|
|
||||||
// setup module being tested
|
// setup module being tested
|
||||||
federator := federation.NewFederator(&suite.state, testrig.NewTestFederatingDB(&suite.state), tc, suite.typeconverter, testrig.NewTestMediaManager(&suite.state))
|
federator := federation.NewFederator(
|
||||||
|
&suite.state,
|
||||||
|
testrig.NewTestFederatingDB(&suite.state),
|
||||||
|
tc,
|
||||||
|
suite.typeconverter,
|
||||||
|
visibility.NewFilter(&suite.state),
|
||||||
|
testrig.NewTestMediaManager(&suite.state),
|
||||||
|
)
|
||||||
|
|
||||||
activity, err := federator.FederatingActor().Send(ctx, testrig.URLMustParse(testAccount.OutboxURI), testActivity)
|
activity, err := federator.FederatingActor().Send(ctx, testrig.URLMustParse(testAccount.OutboxURI), testActivity)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(activity)
|
suite.NotNil(activity)
|
||||||
|
|
||||||
// because zork has no remote followers, sent messages should be empty (no messages sent to own instance)
|
// because zork has no remote followers, sent messages should be empty (no messages sent to own instance)
|
||||||
suite.Empty(httpClient.SentMessages)
|
suite.Empty(&httpClient.SentMessages)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FederatingActorTestSuite) TestSendRemoteFollower() {
|
func (suite *FederatingActorTestSuite) TestSendRemoteFollower() {
|
||||||
|
@ -105,8 +113,16 @@ func (suite *FederatingActorTestSuite) TestSendRemoteFollower() {
|
||||||
|
|
||||||
httpClient := testrig.NewMockHTTPClient(nil, "../../testrig/media")
|
httpClient := testrig.NewMockHTTPClient(nil, "../../testrig/media")
|
||||||
tc := testrig.NewTestTransportController(&suite.state, httpClient)
|
tc := testrig.NewTestTransportController(&suite.state, httpClient)
|
||||||
|
|
||||||
// setup module being tested
|
// setup module being tested
|
||||||
federator := federation.NewFederator(&suite.state, testrig.NewTestFederatingDB(&suite.state), tc, suite.typeconverter, testrig.NewTestMediaManager(&suite.state))
|
federator := federation.NewFederator(
|
||||||
|
&suite.state,
|
||||||
|
testrig.NewTestFederatingDB(&suite.state),
|
||||||
|
tc,
|
||||||
|
suite.typeconverter,
|
||||||
|
visibility.NewFilter(&suite.state),
|
||||||
|
testrig.NewTestMediaManager(&suite.state),
|
||||||
|
)
|
||||||
|
|
||||||
activity, err := federator.FederatingActor().Send(ctx, testrig.URLMustParse(testAccount.OutboxURI), testActivity)
|
activity, err := federator.FederatingActor().Send(ctx, testrig.URLMustParse(testAccount.OutboxURI), testActivity)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"
|
"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||||
|
@ -50,6 +51,7 @@ func NewFederator(
|
||||||
federatingDB federatingdb.DB,
|
federatingDB federatingdb.DB,
|
||||||
transportController transport.Controller,
|
transportController transport.Controller,
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
|
visFilter *visibility.Filter,
|
||||||
mediaManager *media.Manager,
|
mediaManager *media.Manager,
|
||||||
) *Federator {
|
) *Federator {
|
||||||
clock := &Clock{}
|
clock := &Clock{}
|
||||||
|
@ -60,7 +62,7 @@ func NewFederator(
|
||||||
converter: converter,
|
converter: converter,
|
||||||
transportController: transportController,
|
transportController: transportController,
|
||||||
mediaManager: mediaManager,
|
mediaManager: mediaManager,
|
||||||
Dereferencer: dereferencing.NewDereferencer(state, converter, transportController, mediaManager),
|
Dereferencer: dereferencing.NewDereferencer(state, converter, transportController, visFilter, mediaManager),
|
||||||
}
|
}
|
||||||
actor := newFederatingActor(f, f, federatingDB, clock)
|
actor := newFederatingActor(f, f, federatingDB, clock)
|
||||||
f.actor = actor
|
f.actor = actor
|
||||||
|
|
|
@ -39,13 +39,13 @@
|
||||||
malformedKey
|
malformedKey
|
||||||
notRelevantKey
|
notRelevantKey
|
||||||
spamKey
|
spamKey
|
||||||
|
notPermittedKey
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsUnretrievable indicates that a call to retrieve a resource
|
// IsUnretrievable indicates that a call to retrieve a resource
|
||||||
// (account, status, attachment, etc) could not be fulfilled,
|
// (account, status, attachment, etc) could not be fulfilled, either
|
||||||
// either because it was not found locally, or because some
|
// because it was not found locally, or because some prerequisite
|
||||||
// prerequisite remote resource call failed, making it impossible
|
// remote resource call failed, making it impossible to return it.
|
||||||
// to return the item.
|
|
||||||
func IsUnretrievable(err error) bool {
|
func IsUnretrievable(err error) bool {
|
||||||
_, ok := errors.Value(err, unrtrvableKey).(struct{})
|
_, ok := errors.Value(err, unrtrvableKey).(struct{})
|
||||||
return ok
|
return ok
|
||||||
|
@ -57,6 +57,21 @@ func SetUnretrievable(err error) error {
|
||||||
return errors.WithValue(err, unrtrvableKey, struct{}{})
|
return errors.WithValue(err, unrtrvableKey, struct{}{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotPermitted indicates that some call failed due to failed permission
|
||||||
|
// or acceptibility checks. For example an attempt to dereference remote
|
||||||
|
// status in which the status author does not have permission to reply
|
||||||
|
// to the status it is intended to be replying to.
|
||||||
|
func NotPermitted(err error) bool {
|
||||||
|
_, ok := errors.Value(err, notPermittedKey).(struct{})
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNotPermitted will wrap the given error to store a "not permitted"
|
||||||
|
// flag, returning wrapped error. See NotPermitted() for example use-cases.
|
||||||
|
func SetNotPermitted(err error) error {
|
||||||
|
return errors.WithValue(err, notPermittedKey, struct{}{})
|
||||||
|
}
|
||||||
|
|
||||||
// IsWrongType checks error for a stored "wrong type" flag.
|
// IsWrongType checks error for a stored "wrong type" flag.
|
||||||
// Wrong type indicates that an ActivityPub URI returned a
|
// Wrong type indicates that an ActivityPub URI returned a
|
||||||
// type we weren't expecting. For example:
|
// type we weren't expecting. For example:
|
||||||
|
|
|
@ -90,9 +90,17 @@ func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
|
||||||
replyingAccount := suite.testAccounts["remote_account_1"]
|
replyingAccount := suite.testAccounts["remote_account_1"]
|
||||||
|
|
||||||
// Set the replyingAccount's last fetched_at
|
// Set the replyingAccount's last fetched_at
|
||||||
// date to something recent so no refresh is attempted.
|
// date to something recent so no refresh is attempted,
|
||||||
|
// and ensure it isn't a suspended account.
|
||||||
replyingAccount.FetchedAt = time.Now()
|
replyingAccount.FetchedAt = time.Now()
|
||||||
err := suite.state.DB.UpdateAccount(context.Background(), replyingAccount, "fetched_at")
|
replyingAccount.SuspendedAt = time.Time{}
|
||||||
|
replyingAccount.SuspensionOrigin = ""
|
||||||
|
err := suite.state.DB.UpdateAccount(context.Background(),
|
||||||
|
replyingAccount,
|
||||||
|
"fetched_at",
|
||||||
|
"suspended_at",
|
||||||
|
"suspension_origin",
|
||||||
|
)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// Get replying statusable to use from remote test statuses.
|
// Get replying statusable to use from remote test statuses.
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||||
|
@ -27,5 +28,5 @@
|
||||||
|
|
||||||
// NewTestFederator returns a federator with the given database and (mock!!) transport controller.
|
// NewTestFederator returns a federator with the given database and (mock!!) transport controller.
|
||||||
func NewTestFederator(state *state.State, tc transport.Controller, mediaManager *media.Manager) *federation.Federator {
|
func NewTestFederator(state *state.State, tc transport.Controller, mediaManager *media.Manager) *federation.Federator {
|
||||||
return federation.NewFederator(state, NewTestFederatingDB(state), tc, typeutils.NewConverter(state), mediaManager)
|
return federation.NewFederator(state, NewTestFederatingDB(state), tc, typeutils.NewConverter(state), visibility.NewFilter(state), mediaManager)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue