Skip to content

Commit aebb846

Browse files
NyaaaWhatsUpDoctsmethurst
authored andcommitted
[bugfix] harden checks for remotes masquerading as local, and return correct local account redirects early (#3706)
1 parent 6f4cb2f commit aebb846

File tree

3 files changed

+146
-22
lines changed

3 files changed

+146
-22
lines changed

internal/federation/dereferencing/account.go

+17-8
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,16 @@ func (d *Dereferencer) enrichAccount(
639639
return nil, nil, gtserror.Newf("db error getting account after redirects: %w", err)
640640
}
641641

642-
if alreadyAcc != nil {
642+
switch {
643+
case alreadyAcc == nil:
644+
// nothing to do
645+
646+
case alreadyAcc.IsLocal():
647+
// Request eventually redirected to a
648+
// local account. Return it as-is here.
649+
return alreadyAcc, nil, nil
650+
651+
default:
643652
// We had this account stored
644653
// under discovered final URI.
645654
//
@@ -718,12 +727,6 @@ func (d *Dereferencer) enrichAccount(
718727
latestAcc.Username = cmp.Or(latestAcc.Username, accUsername)
719728
}
720729

721-
if latestAcc.Domain == "" {
722-
// Ensure we have a domain set by this point,
723-
// otherwise it gets stored as a local user!
724-
return nil, nil, gtserror.Newf("empty domain for %s", uri)
725-
}
726-
727730
// Ensure the final parsed account URI matches
728731
// the input URI we fetched (or received) it as.
729732
if matches, err := util.URIMatches(
@@ -740,10 +743,16 @@ func (d *Dereferencer) enrichAccount(
740743
} else if !matches {
741744
return nil, nil, gtserror.Newf(
742745
"account uri %s does not match %s",
743-
latestAcc.URI, uri.String(),
746+
latestAcc.URI, uri,
744747
)
745748
}
746749

750+
// Ensure this isn't a local account,
751+
// or a remote masquerading as such!
752+
if latestAcc.IsLocal() {
753+
return nil, nil, gtserror.Newf("cannot dereference local account %s", uri)
754+
}
755+
747756
// Get current time.
748757
now := time.Now()
749758

internal/federation/dereferencing/account_test.go

+110
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@
1818
package dereferencing_test
1919

2020
import (
21+
"bytes"
2122
"context"
2223
"crypto/rsa"
2324
"crypto/x509"
25+
"encoding/json"
2426
"encoding/pem"
2527
"fmt"
28+
"io"
29+
"net/http"
2630
"net/url"
2731
"testing"
2832
"time"
@@ -33,6 +37,7 @@ import (
3337
"github.com/superseriousbusiness/gotosocial/internal/ap"
3438
"github.com/superseriousbusiness/gotosocial/internal/config"
3539
"github.com/superseriousbusiness/gotosocial/internal/db"
40+
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
3641
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
3742
"github.com/superseriousbusiness/gotosocial/testrig"
3843
)
@@ -214,6 +219,111 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountWithUnknownUserURI() {
214219
suite.Nil(fetchedAccount)
215220
}
216221

222+
func (suite *AccountTestSuite) TestDereferenceLocalAccountByRedirect() {
223+
ctx, cncl := context.WithCancel(context.Background())
224+
defer cncl()
225+
226+
fetchingAccount := suite.testAccounts["local_account_1"]
227+
targetAccount := suite.testAccounts["local_account_2"]
228+
229+
// Convert the target account to ActivityStreams model for dereference.
230+
targetAccountable, err := suite.converter.AccountToAS(ctx, targetAccount)
231+
suite.NoError(err)
232+
suite.NotNil(targetAccountable)
233+
234+
// Serialize to "raw" JSON map for response.
235+
rawJSON, err := ap.Serialize(targetAccountable)
236+
suite.NoError(err)
237+
238+
// Finally serialize to actual bytes.
239+
json, err := json.Marshal(rawJSON)
240+
suite.NoError(err)
241+
242+
// Replace test HTTP client with one that always returns the target account AS model.
243+
suite.client = testrig.NewMockHTTPClient(func(req *http.Request) (*http.Response, error) {
244+
return &http.Response{
245+
Status: http.StatusText(http.StatusOK),
246+
StatusCode: http.StatusOK,
247+
ContentLength: int64(len(json)),
248+
Header: http.Header{"Content-Type": {"application/activity+json"}},
249+
Body: io.NopCloser(bytes.NewReader(json)),
250+
Request: &http.Request{URL: testrig.URLMustParse(targetAccount.URI)},
251+
}, nil
252+
}, "")
253+
254+
// Update dereferencer to use new test HTTP client.
255+
suite.dereferencer = dereferencing.NewDereferencer(
256+
&suite.state,
257+
suite.converter,
258+
testrig.NewTestTransportController(&suite.state, suite.client),
259+
suite.visFilter,
260+
suite.intFilter,
261+
suite.media,
262+
)
263+
264+
// Use any old input test URI, this doesn't actually matter what it is.
265+
uri := testrig.URLMustParse("https://this-will-be-redirected.butts/")
266+
267+
// Try dereference the test URI, since it correctly redirects to us it should return our account.
268+
account, accountable, err := suite.dereferencer.GetAccountByURI(ctx, fetchingAccount.Username, uri)
269+
suite.NoError(err)
270+
suite.Nil(accountable)
271+
suite.NotNil(account)
272+
suite.Equal(targetAccount.ID, account.ID)
273+
}
274+
275+
func (suite *AccountTestSuite) TestDereferenceMasqueradingLocalAccount() {
276+
ctx, cncl := context.WithCancel(context.Background())
277+
defer cncl()
278+
279+
fetchingAccount := suite.testAccounts["local_account_1"]
280+
targetAccount := suite.testAccounts["local_account_2"]
281+
282+
// Convert the target account to ActivityStreams model for dereference.
283+
targetAccountable, err := suite.converter.AccountToAS(ctx, targetAccount)
284+
suite.NoError(err)
285+
suite.NotNil(targetAccountable)
286+
287+
// Serialize to "raw" JSON map for response.
288+
rawJSON, err := ap.Serialize(targetAccountable)
289+
suite.NoError(err)
290+
291+
// Finally serialize to actual bytes.
292+
json, err := json.Marshal(rawJSON)
293+
suite.NoError(err)
294+
295+
// Use any old input test URI, this doesn't actually matter what it is.
296+
uri := testrig.URLMustParse("https://this-will-be-redirected.butts/")
297+
298+
// Replace test HTTP client with one that returns OUR account, but at their URI endpoint.
299+
suite.client = testrig.NewMockHTTPClient(func(req *http.Request) (*http.Response, error) {
300+
return &http.Response{
301+
Status: http.StatusText(http.StatusOK),
302+
StatusCode: http.StatusOK,
303+
ContentLength: int64(len(json)),
304+
Header: http.Header{"Content-Type": {"application/activity+json"}},
305+
Body: io.NopCloser(bytes.NewReader(json)),
306+
Request: &http.Request{URL: uri},
307+
}, nil
308+
}, "")
309+
310+
// Update dereferencer to use new test HTTP client.
311+
suite.dereferencer = dereferencing.NewDereferencer(
312+
&suite.state,
313+
suite.converter,
314+
testrig.NewTestTransportController(&suite.state, suite.client),
315+
suite.visFilter,
316+
suite.intFilter,
317+
suite.media,
318+
)
319+
320+
// Try dereference the test URI, since it correctly redirects to us it should return our account.
321+
account, accountable, err := suite.dereferencer.GetAccountByURI(ctx, fetchingAccount.Username, uri)
322+
suite.NotNil(err)
323+
suite.Nil(account)
324+
suite.Nil(accountable)
325+
}
326+
217327
func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithNonMatchingURI() {
218328
fetchingAccount := suite.testAccounts["local_account_1"]
219329

internal/federation/dereferencing/dereferencer_test.go

+19-14
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
2626
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
2727
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
28+
"github.com/superseriousbusiness/gotosocial/internal/media"
2829
"github.com/superseriousbusiness/gotosocial/internal/state"
2930
"github.com/superseriousbusiness/gotosocial/internal/storage"
3031
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
@@ -33,10 +34,14 @@ import (
3334

3435
type DereferencerStandardTestSuite struct {
3536
suite.Suite
36-
db db.DB
37-
storage *storage.Driver
38-
state state.State
39-
client *testrig.MockHTTPClient
37+
db db.DB
38+
storage *storage.Driver
39+
state state.State
40+
client *testrig.MockHTTPClient
41+
converter *typeutils.Converter
42+
visFilter *visibility.Filter
43+
intFilter *interaction.Filter
44+
media *media.Manager
4045

4146
testRemoteStatuses map[string]vocab.ActivityStreamsNote
4247
testRemotePeople map[string]vocab.ActivityStreamsPerson
@@ -66,32 +71,32 @@ func (suite *DereferencerStandardTestSuite) SetupTest() {
6671

6772
suite.db = testrig.NewTestDB(&suite.state)
6873

69-
converter := typeutils.NewConverter(&suite.state)
74+
suite.converter = typeutils.NewConverter(&suite.state)
75+
suite.visFilter = visibility.NewFilter(&suite.state)
76+
suite.intFilter = interaction.NewFilter(&suite.state)
77+
suite.media = testrig.NewTestMediaManager(&suite.state)
7078

7179
testrig.StartTimelines(
7280
&suite.state,
73-
visibility.NewFilter(&suite.state),
74-
converter,
81+
suite.visFilter,
82+
suite.converter,
7583
)
7684

7785
suite.client = testrig.NewMockHTTPClient(nil, "../../../testrig/media")
7886
suite.storage = testrig.NewInMemoryStorage()
7987
suite.state.DB = suite.db
8088
suite.state.Storage = suite.storage
8189

82-
visFilter := visibility.NewFilter(&suite.state)
83-
intFilter := interaction.NewFilter(&suite.state)
84-
media := testrig.NewTestMediaManager(&suite.state)
8590
suite.dereferencer = dereferencing.NewDereferencer(
8691
&suite.state,
87-
converter,
92+
suite.converter,
8893
testrig.NewTestTransportController(
8994
&suite.state,
9095
suite.client,
9196
),
92-
visFilter,
93-
intFilter,
94-
media,
97+
suite.visFilter,
98+
suite.intFilter,
99+
suite.media,
95100
)
96101
testrig.StandardDBSetup(suite.db, nil)
97102
}

0 commit comments

Comments
 (0)