Improve shared status verification (#2525)

* Instead of parsing shared status contents verbatim, make roundtrip
to purported original URL. Confirm that the "original" URL is from the
same domain as the author it claims to be from.

* Fix obvious typo, add comment

* Use URI look-up first

* Add test, update Goldfinger dependency to make less useless HTTP requests per Webfinger lookup
th-downstream
Eugen Rochko 8 years ago committed by GitHub
parent 64bef924ae
commit 97dff125a8

@ -165,7 +165,7 @@ GEM
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
globalid (0.3.7) globalid (0.3.7)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
goldfinger (1.1.2) goldfinger (1.2.0)
addressable (~> 2.4) addressable (~> 2.4)
http (~> 2.0) http (~> 2.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
@ -459,7 +459,7 @@ GEM
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.3) unf_ext (0.0.7.4)
unicode-display_width (1.1.3) unicode-display_width (1.1.3)
uniform_notifier (1.10.0) uniform_notifier (1.10.0)
warden (1.2.7) warden (1.2.7)

@ -39,9 +39,19 @@ class FetchRemoteStatusService < BaseService
Rails.logger.debug "Going to webfinger #{username}@#{domain}" Rails.logger.debug "Going to webfinger #{username}@#{domain}"
return FollowRemoteAccountService.new.call("#{username}@#{domain}") account = FollowRemoteAccountService.new.call("#{username}@#{domain}")
# If the author's confirmed URLs do not match the domain of the URL
# we are reading this from, abort
return nil unless confirmed_domain?(domain, account)
account
rescue Nokogiri::XML::XPath::SyntaxError rescue Nokogiri::XML::XPath::SyntaxError
Rails.logger.debug 'Invalid XML or missing namespace' Rails.logger.debug 'Invalid XML or missing namespace'
nil nil
end end
def confirmed_domain?(domain, account)
domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url).normalize.host).zero?
end
end end

@ -47,7 +47,7 @@ class ProcessFeedService < BaseService
return status unless just_created return status unless just_created
if verb == :share if verb == :share
original_status, = status_from_xml(@xml.at_xpath('.//activity:object', activity: TagManager::AS_XMLNS)) original_status = shared_status_from_xml(@xml.at_xpath('.//activity:object', activity: TagManager::AS_XMLNS))
status.reblog = original_status status.reblog = original_status
if original_status.nil? if original_status.nil?
@ -90,6 +90,14 @@ class ProcessFeedService < BaseService
!([:post, :share, :delete].include?(verb) && [:activity, :note, :comment].include?(type)) !([:post, :share, :delete].include?(verb) && [:activity, :note, :comment].include?(type))
end end
def shared_status_from_xml(entry)
status = find_status(id(entry))
return status unless status.nil?
FetchRemoteStatusService.new.call(url(entry))
end
def status_from_xml(entry) def status_from_xml(entry)
# Return early if status already exists in db # Return early if status already exists in db
status = find_status(id(entry)) status = find_status(id(entry))

@ -5,6 +5,7 @@ RSpec.describe FollowRemoteAccountService do
before do before do
stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt')) stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:catsrgr8@example.com").to_return(status: 404)
stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404) stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404)
stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt')) stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt'))
stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no").to_return(status: 404) stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no").to_return(status: 404)

@ -1,11 +1,12 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe ProcessFeedService do RSpec.describe ProcessFeedService do
subject { ProcessFeedService.new }
describe 'processing a feed' do
let(:body) { File.read(File.join(Rails.root, 'spec', 'fixtures', 'xml', 'mastodon.atom')) } let(:body) { File.read(File.join(Rails.root, 'spec', 'fixtures', 'xml', 'mastodon.atom')) }
let(:account) { Fabricate(:account, username: 'localhost', domain: 'kickass.zone') } let(:account) { Fabricate(:account, username: 'localhost', domain: 'kickass.zone') }
subject { ProcessFeedService.new }
before do before do
stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {}) stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {})
stub_request(:get, "http://kickass.zone/system/accounts/avatars/000/000/001/large/eris.png").to_return(request_fixture('avatar.txt')) stub_request(:get, "http://kickass.zone/system/accounts/avatars/000/000/001/large/eris.png").to_return(request_fixture('avatar.txt'))
@ -50,3 +51,68 @@ RSpec.describe ProcessFeedService do
expect(status.media_attachments.first).to have_attached_file(:file) expect(status.media_attachments.first).to have_attached_file(:file)
end end
end end
it 'does not accept tampered reblogs' do
good_actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com')
real_body = <<XML
<?xml version="1.0"?>
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0">
<id>tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status</id>
<published>2017-04-27T13:49:25Z</published>
<updated>2017-04-27T13:49:25Z</updated>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<author>
<id>https://overwatch.com/users/tracer</id>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<uri>https://overwatch.com/users/tracer</uri>
<name>tracer</name>
</author>
<content type="html">Overwatch rocks</content>
</entry>
XML
stub_request(:head, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 200, headers: { 'Content-Type' => 'application/atom+xml' })
stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 200, body: real_body)
bad_actor = Fabricate(:account, username: 'sombra', domain: 'talon.xyz')
body = <<XML
<?xml version="1.0"?>
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0">
<id>tag:talon.xyz,2017-04-27:objectId=4467137:objectType=Status</id>
<published>2017-04-27T13:49:25Z</published>
<updated>2017-04-27T13:49:25Z</updated>
<author>
<id>https://talon.xyz/users/sombra</id>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<uri>https://talon.xyz/users/sombra</uri>
<name>sombra</name>
</author>
<activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
<content type="html">Overwatch SUCKS AHAHA</content>
<activity:object>
<id>tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status</id>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<author>
<id>https://overwatch.com/users/tracer</id>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<uri>https://overwatch.com/users/tracer</uri>
<name>tracer</name>
</author>
<content type="html">Overwatch SUCKS AHAHA</content>
<link rel="alternate" type="text/html" href="https://overwatch.com/users/tracer/updates/1" />
</activity:object>
</entry>
XML
created_statuses = subject.call(body, bad_actor)
expect(created_statuses.first.reblog?).to be true
expect(created_statuses.first.account_id).to eq bad_actor.id
expect(created_statuses.first.reblog.account_id).to eq good_actor.id
expect(created_statuses.first.reblog.text).to eq 'Overwatch rocks'
end
end

Loading…
Cancel
Save