FEATURE: Support for a whitelist for embeddable host paths

This commit is contained in:
Robin Ward 2016-08-23 14:55:52 -04:00
parent 43a3210c20
commit c3a3aff120
12 changed files with 51 additions and 31 deletions

View file

@ -30,10 +30,11 @@ export default Ember.Component.extend(bufferedProperty('host'), {
save() { save() {
if (this.get('cantSave')) { return; } if (this.get('cantSave')) { return; }
const props = this.get('buffered').getProperties('host'); const props = this.get('buffered').getProperties('host', 'path_whitelist');
props.category_id = this.get('categoryId'); props.category_id = this.get('categoryId');
const host = this.get('host'); const host = this.get('host');
host.save(props).then(() => { host.save(props).then(() => {
host.set('category', Discourse.Category.findById(this.get('categoryId'))); host.set('category', Discourse.Category.findById(this.get('categoryId')));
this.set('editToggled', false); this.set('editToggled', false);

View file

@ -2,6 +2,9 @@
<td> <td>
{{input value=buffered.host placeholder="example.com" enter="save" class="host-name"}} {{input value=buffered.host placeholder="example.com" enter="save" class="host-name"}}
</td> </td>
<td>
{{input value=buffered.path_whitelist placeholder="/blog/.*" enter="save" class="path-whitelist"}}
</td>
<td> <td>
{{category-chooser value=categoryId}} {{category-chooser value=categoryId}}
</td> </td>
@ -11,6 +14,7 @@
</td> </td>
{{else}} {{else}}
<td>{{host.host}}</td> <td>{{host.host}}</td>
<td>{{host.path_whitelist}}</td>
<td>{{category-badge host.category}}</td> <td>{{category-badge host.category}}</td>
<td> <td>
{{d-button icon="pencil" action="edit"}} {{d-button icon="pencil" action="edit"}}

View file

@ -2,9 +2,10 @@
{{#if embedding.embeddable_hosts}} {{#if embedding.embeddable_hosts}}
<table class='embedding'> <table class='embedding'>
<tr> <tr>
<th style='width: 50%'>{{i18n "admin.embedding.host"}}</th> <th style='width: 30%'>{{i18n "admin.embedding.host"}}</th>
<th style='width: 30%'>{{i18n "admin.embedding.path_whitelist"}}</th>
<th style='width: 30%'>{{i18n "admin.embedding.category"}}</th> <th style='width: 30%'>{{i18n "admin.embedding.category"}}</th>
<th style='width: 20%'>&nbsp;</th> <th style='width: 10%'>&nbsp;</th>
</tr> </tr>
{{#each embedding.embeddable_hosts as |host|}} {{#each embedding.embeddable_hosts as |host|}}
{{embeddable-host host=host deleteHost="deleteHost"}} {{embeddable-host host=host deleteHost="deleteHost"}}

View file

@ -21,6 +21,7 @@ class Admin::EmbeddableHostsController < Admin::AdminController
def save_host(host) def save_host(host)
host.host = params[:embeddable_host][:host] host.host = params[:embeddable_host][:host]
host.path_whitelist = params[:embeddable_host][:path_whitelist]
host.category_id = params[:embeddable_host][:category_id] host.category_id = params[:embeddable_host][:category_id]
host.category_id = SiteSetting.uncategorized_category_id if host.category_id.blank? host.category_id = SiteSetting.uncategorized_category_id if host.category_id.blank?

View file

@ -85,7 +85,7 @@ class EmbedController < ApplicationController
def ensure_embeddable def ensure_embeddable
if !(Rails.env.development? && current_user.try(:admin?)) if !(Rails.env.development? && current_user.try(:admin?))
raise Discourse::InvalidAccess.new('invalid referer host') unless EmbeddableHost.host_allowed?(request.referer) raise Discourse::InvalidAccess.new('invalid referer host') unless EmbeddableHost.url_allowed?(request.referer)
end end
response.headers['X-Frame-Options'] = "ALLOWALL" response.headers['X-Frame-Options'] = "ALLOWALL"

View file

@ -7,8 +7,11 @@ class EmbeddableHost < ActiveRecord::Base
self.host.sub!(/\/.*$/, '') self.host.sub!(/\/.*$/, '')
end end
def self.record_for_host(host) def self.record_for_url(uri)
uri = URI(host) rescue nil
if uri.is_a?(String)
uri = URI(uri) rescue nil
end
return false unless uri.present? return false unless uri.present?
host = uri.host host = uri.host
@ -17,8 +20,13 @@ class EmbeddableHost < ActiveRecord::Base
where("lower(host) = ?", host).first where("lower(host) = ?", host).first
end end
def self.host_allowed?(host) def self.url_allowed?(url)
record_for_host(host).present? uri = URI(url) rescue nil
return false unless uri.present?
host = record_for_url(uri)
return host.present? &&
(host.path_whitelist.blank? || !Regexp.new(host.path_whitelist).match(uri.path).nil?)
end end
private private

View file

@ -33,7 +33,7 @@ class TopicEmbed < ActiveRecord::Base
# If there is no embed, create a topic, post and the embed. # If there is no embed, create a topic, post and the embed.
if embed.blank? if embed.blank?
Topic.transaction do Topic.transaction do
eh = EmbeddableHost.record_for_host(url) eh = EmbeddableHost.record_for_url(url)
creator = PostCreator.new(user, creator = PostCreator.new(user,
title: title, title: title,

View file

@ -1,16 +1,11 @@
class EmbeddableHostSerializer < ApplicationSerializer class EmbeddableHostSerializer < ApplicationSerializer
attributes :id, :host, :category_id
def id TO_SERIALIZE = [:id, :host, :path_whitelist, :category_id]
object.id
attributes *TO_SERIALIZE
TO_SERIALIZE.each do |attr|
define_method(attr) { object.send(attr) }
end end
def host
object.host
end
def category_id
object.category_id
end
end end

View file

@ -3113,6 +3113,7 @@ en:
sample: "Use the following HTML code into your site to create and embed discourse topics. Replace <b>REPLACE_ME</b> with the canonical URL of the page you are embedding it on." sample: "Use the following HTML code into your site to create and embed discourse topics. Replace <b>REPLACE_ME</b> with the canonical URL of the page you are embedding it on."
title: "Embedding" title: "Embedding"
host: "Allowed Hosts" host: "Allowed Hosts"
path_whitelist: "Path Whitelist"
edit: "edit" edit: "edit"
category: "Post to Category" category: "Post to Category"
add_host: "Add Host" add_host: "Add Host"

View file

@ -7,13 +7,13 @@ class TopicRetriever
end end
def retrieve def retrieve
perform_retrieve unless (invalid_host? || retrieved_recently?) perform_retrieve unless (invalid_url? || retrieved_recently?)
end end
private private
def invalid_host? def invalid_url?
!EmbeddableHost.host_allowed?(@embed_url) !EmbeddableHost.url_allowed?(@embed_url)
end end
def retrieved_recently? def retrieved_recently?

View file

@ -10,7 +10,7 @@ describe TopicRetriever do
describe "#retrieve" do describe "#retrieve" do
context "when host is invalid" do context "when host is invalid" do
before do before do
topic_retriever.stubs(:invalid_host?).returns(true) topic_retriever.stubs(:invalid_url?).returns(true)
end end
it "does not perform_retrieve" do it "does not perform_retrieve" do
@ -32,7 +32,7 @@ describe TopicRetriever do
context "when host is not invalid" do context "when host is not invalid" do
before do before do
topic_retriever.stubs(:invalid_host?).returns(false) topic_retriever.stubs(:invalid_url?).returns(false)
end end
context "when topics have been retrieived recently" do context "when topics have been retrieived recently" do

View file

@ -49,19 +49,28 @@ describe EmbeddableHost do
expect(eh).not_to be_valid expect(eh).not_to be_valid
end end
describe "allows_embeddable_host" do describe "url_allowed?" do
let!(:host) { Fabricate(:embeddable_host) } let!(:host) { Fabricate(:embeddable_host) }
it 'works as expected' do it 'works as expected' do
expect(EmbeddableHost.host_allowed?('http://eviltrout.com')).to eq(true) expect(EmbeddableHost.url_allowed?('http://eviltrout.com')).to eq(true)
expect(EmbeddableHost.host_allowed?('https://eviltrout.com')).to eq(true) expect(EmbeddableHost.url_allowed?('https://eviltrout.com')).to eq(true)
expect(EmbeddableHost.host_allowed?('https://not-eviltrout.com')).to eq(false) expect(EmbeddableHost.url_allowed?('https://not-eviltrout.com')).to eq(false)
end end
it 'works with multiple hosts' do it 'works with multiple hosts' do
Fabricate(:embeddable_host, host: 'discourse.org') Fabricate(:embeddable_host, host: 'discourse.org')
expect(EmbeddableHost.host_allowed?('http://eviltrout.com')).to eq(true) expect(EmbeddableHost.url_allowed?('http://eviltrout.com')).to eq(true)
expect(EmbeddableHost.host_allowed?('http://discourse.org')).to eq(true) expect(EmbeddableHost.url_allowed?('http://discourse.org')).to eq(true)
end
end
describe "path_whitelist" do
let!(:host) { Fabricate(:embeddable_host, path_whitelist: '^/fp/\d{4}/\d{2}/\d{2}/.*$') }
it "matches the path" do
expect(EmbeddableHost.url_allowed?('http://eviltrout.com')).to eq(false)
expect(EmbeddableHost.url_allowed?('http://eviltrout.com/fp/2016/08/25/test-page')).to eq(true)
end end
end end