Skip to content

Commit

Permalink
Improve signature verification safeguards (mastodon#8959)
Browse files Browse the repository at this point in the history
* Downcase signed_headers string before building the signed string

The HTTP Signatures draft does not mandate the “headers” field to be downcased,
but mandates the header field names to be downcased in the signed string, which
means that prior to this patch, Mastodon could fail to process signatures from
some compliant clients. It also means that it would not actually check the
Digest of non-compliant clients that wouldn't use a lowercased Digest field
name.

Thankfully, I don't know of any such client.

* Revert "Remove dead code (mastodon#8919)"

This reverts commit a00ce8c.

* Restore time window checking, change it to 12 hours

By checking the Date header, we can prevent replaying old vulnerable
signatures. The focus is to prevent replaying old vulnerable requests
from software that has been fixed in the meantime, so a somewhat long
window should be fine and accounts for timezone misconfiguration.

* Escape users' URLs when formatting them

Fixes possible HTML injection

* Escape all string interpolations in Formatter class

Slightly improve performance by reducing class allocations
from repeated Formatter#encode calls

* Fix code style issues
  • Loading branch information
Gargron authored and rtucker committed Oct 12, 2018
1 parent 3d29c81 commit e3adb6a
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 7 deletions.
18 changes: 17 additions & 1 deletion app/controllers/concerns/signature_verification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ def signed_request_account
return
end

if request.headers['Date'].present? && !matches_time_window?
@signature_verification_failure_reason = 'Signed request date outside acceptable time window'
@signed_request_account = nil
return
end

raw_signature = request.headers['Signature']
signature_params = {}

Expand Down Expand Up @@ -76,7 +82,7 @@ def request_body
def build_signed_string(signed_headers)
signed_headers = 'date' if signed_headers.blank?

signed_headers.split(' ').map do |signed_header|
signed_headers.downcase.split(' ').map do |signed_header|
if signed_header == Request::REQUEST_TARGET
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
elsif signed_header == 'digest'
Expand All @@ -87,6 +93,16 @@ def build_signed_string(signed_headers)
end.join("\n")
end

def matches_time_window?
begin
time_sent = Time.httpdate(request.headers['Date'])
rescue ArgumentError
return false
end

(Time.now.utc - time_sent).abs <= 12.hours
end

def body_digest
"SHA-256=#{Digest::SHA256.base64digest(request_body)}"
end
Expand Down
16 changes: 10 additions & 6 deletions app/lib/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,12 @@ def linkify(text)

private

def html_entities
@html_entities ||= HTMLEntities.new
end

def encode(html)
HTMLEntities.new.encode(html)
html_entities.encode(html)
end

def encode_and_link_urls(html, accounts = nil, options = {})
Expand Down Expand Up @@ -143,7 +147,7 @@ def encode_custom_emojis(html, emojis, animate = false)
emoji = emoji_map[shortcode]

if emoji
replacement = "<img draggable=\"false\" class=\"emojione\" alt=\":#{shortcode}:\" title=\":#{shortcode}:\" src=\"#{emoji}\" />"
replacement = "<img draggable=\"false\" class=\"emojione\" alt=\":#{encode(shortcode)}:\" title=\":#{encode(shortcode)}:\" src=\"#{encode(emoji)}\" />"
before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : ''
html = before_html + replacement + html[i + 1..-1]
i += replacement.size - (shortcode.size + 2) - 1
Expand Down Expand Up @@ -212,7 +216,7 @@ def link_to_mention(entity, linkable_accounts)
return link_to_account(acct) unless linkable_accounts

account = linkable_accounts.find { |item| TagManager.instance.same_acct?(item.acct, acct) }
account ? mention_html(account) : "@#{acct}"
account ? mention_html(account) : "@#{encode(acct)}"
end

def link_to_account(acct)
Expand All @@ -221,7 +225,7 @@ def link_to_account(acct)
domain = nil if TagManager.instance.local_domain?(domain)
account = EntityCache.instance.mention(username, domain)

account ? mention_html(account) : "@#{acct}"
account ? mention_html(account) : "@#{encode(acct)}"
end

def link_to_hashtag(entity)
Expand All @@ -239,10 +243,10 @@ def link_html(url)
end

def hashtag_html(tag)
"<a href=\"#{tag_url(tag.downcase)}\" class=\"mention hashtag\" rel=\"tag\">#<span>#{tag}</span></a>"
"<a href=\"#{encode(tag_url(tag.downcase))}\" class=\"mention hashtag\" rel=\"tag\">#<span>#{encode(tag)}</span></a>"
end

def mention_html(account)
"<span class=\"h-card\"><a href=\"#{TagManager.instance.url_for(account)}\" class=\"u-url mention\">@<span>#{account.username}</span></a></span>"
"<span class=\"h-card\"><a href=\"#{encode(TagManager.instance.url_for(account))}\" class=\"u-url mention\">@<span>#{encode(account.username)}</span></a></span>"
end
end
24 changes: 24 additions & 0 deletions spec/controllers/concerns/signature_verification_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,30 @@ def alternative_success
end
end

context 'with request older than a day' do
before do
get :success

fake_request = Request.new(:get, request.url)
fake_request.add_headers({ 'Date' => 2.days.ago.utc.httpdate })
fake_request.on_behalf_of(author)

request.headers.merge!(fake_request.headers)
end

describe '#signed_request?' do
it 'returns true' do
expect(controller.signed_request?).to be true
end
end

describe '#signed_request_account' do
it 'returns nil' do
expect(controller.signed_request_account).to be_nil
end
end
end

context 'with body' do
before do
post :success, body: 'Hello world'
Expand Down

0 comments on commit e3adb6a

Please sign in to comment.