-
Notifications
You must be signed in to change notification settings - Fork 0
Enhance embed URL handling and validation system #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: embed-url-handling-pre
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| /* global discourseUrl */ | ||
| /* global discourseEmbedUrl */ | ||
| (function() { | ||
|
|
||
| var comments = document.getElementById('discourse-comments'), | ||
| iframe = document.createElement('iframe'); | ||
| iframe.src = discourseUrl + "embed/best?embed_url=" + encodeURIComponent(discourseEmbedUrl); | ||
| iframe.id = 'discourse-embed-frame'; | ||
| iframe.width = "100%"; | ||
| iframe.frameBorder = "0"; | ||
| iframe.scrolling = "no"; | ||
| comments.appendChild(iframe); | ||
|
|
||
|
|
||
| function postMessageReceived(e) { | ||
| if (!e) { return; } | ||
| if (discourseUrl.indexOf(e.origin) === -1) { return; } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security vulnerability: Weak origin validation. The current origin validation using Use proper origin validation: - if (discourseUrl.indexOf(e.origin) === -1) { return; }
+ var allowedOrigin = new URL(discourseUrl).origin;
+ if (e.origin !== allowedOrigin) { return; }🤖 Prompt for AI Agents |
||
|
|
||
| if (e.data) { | ||
| if (e.data.type === 'discourse-resize' && e.data.height) { | ||
| iframe.height = e.data.height + "px"; | ||
| } | ||
| } | ||
| } | ||
| window.addEventListener('message', postMessageReceived, false); | ||
|
|
||
| })(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| //= require ./vendor/normalize | ||
| //= require ./common/foundation/base | ||
|
|
||
| article.post { | ||
| border-bottom: 1px solid #ddd; | ||
|
|
||
| .post-date { | ||
| float: right; | ||
| color: #aaa; | ||
| font-size: 12px; | ||
| margin: 4px 4px 0 0; | ||
| } | ||
|
|
||
| .author { | ||
| padding: 20px 0; | ||
| width: 92px; | ||
| float: left; | ||
|
|
||
| text-align: center; | ||
|
|
||
| h3 { | ||
| text-align: center; | ||
| color: #4a6b82; | ||
| font-size: 13px; | ||
| margin: 0; | ||
| } | ||
| } | ||
|
|
||
| .cooked { | ||
| padding: 20px 0; | ||
| margin-left: 92px; | ||
|
|
||
| p { | ||
| margin: 0 0 1em 0; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| header { | ||
| padding: 10px 10px 20px 10px; | ||
|
|
||
| font-size: 18px; | ||
|
|
||
| border-bottom: 1px solid #ddd; | ||
| } | ||
|
|
||
| footer { | ||
| font-size: 18px; | ||
|
|
||
| .logo { | ||
| margin-right: 10px; | ||
| margin-top: 10px; | ||
| } | ||
|
|
||
| a[href].button { | ||
| margin: 10px 0 0 10px; | ||
| } | ||
| } | ||
|
|
||
| .logo { | ||
| float: right; | ||
| max-height: 30px; | ||
| } | ||
|
|
||
| a[href].button { | ||
| background-color: #eee; | ||
| padding: 5px; | ||
| display: inline-block; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class EmbedController < ApplicationController | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| skip_before_filter :check_xhr | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| skip_before_filter :preload_json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| before_filter :ensure_embeddable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| layout 'embed' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def best | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| embed_url = params.require(:embed_url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| topic_id = TopicEmbed.topic_id_for_embed(embed_url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if topic_id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @topic_view = TopicView.new(topic_id, current_user, {best: 5}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Jobs.enqueue(:retrieve_topic, user_id: current_user.try(:id), embed_url: embed_url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render 'loading' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| discourse_expires_in 1.minute | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add explicit rendering and improve error handling. The method logic is sound but has a few areas for improvement:
Apply this diff to improve the method: def best
embed_url = params.require(:embed_url)
+
+ # Validate URL format
+ begin
+ URI.parse(embed_url)
+ rescue URI::InvalidURIError
+ render json: { error: 'Invalid embed URL' }, status: 400
+ return
+ end
+
topic_id = TopicEmbed.topic_id_for_embed(embed_url)
if topic_id
@topic_view = TopicView.new(topic_id, current_user, {best: 5})
+ render 'best'
+ discourse_expires_in 1.minute
else
Jobs.enqueue(:retrieve_topic, user_id: current_user.try(:id), embed_url: embed_url)
render 'loading'
end
-
- discourse_expires_in 1.minute
end📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def ensure_embeddable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise Discourse::InvalidAccess.new('embeddable host not set') if SiteSetting.embeddable_host.blank? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise Discourse::InvalidAccess.new('invalid referer host') if URI(request.referer || '').host != SiteSetting.embeddable_host | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response.headers['X-Frame-Options'] = "ALLOWALL" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rescue URI::InvalidURIError | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise Discourse::InvalidAccess.new('invalid referer host') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+24
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Strengthen embedding security controls. The security validation is functional but could be more robust:
Consider this more secure approach: def ensure_embeddable
raise Discourse::InvalidAccess.new('embeddable host not set') if SiteSetting.embeddable_host.blank?
- raise Discourse::InvalidAccess.new('invalid referer host') if URI(request.referer || '').host != SiteSetting.embeddable_host
+
+ referer = request.referer
+ raise Discourse::InvalidAccess.new('referer required for embedding') if referer.blank?
+ raise Discourse::InvalidAccess.new('invalid referer host') if URI(referer).host != SiteSetting.embeddable_host
- response.headers['X-Frame-Options'] = "ALLOWALL"
+ # Only allow framing from the configured embeddable host
+ response.headers['X-Frame-Options'] = "ALLOW-FROM https://#{SiteSetting.embeddable_host}"
+ response.headers['Content-Security-Policy'] = "frame-ancestors https://#{SiteSetting.embeddable_host}"
rescue URI::InvalidURIError
- raise Discourse::InvalidAccess.new('invalid referer host')
+ raise Discourse::InvalidAccess.new('invalid referer URL')
end📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| require_dependency 'email/sender' | ||
| require_dependency 'topic_retriever' | ||
|
|
||
| module Jobs | ||
|
|
||
| # Asynchronously retrieve a topic from an embedded site | ||
| class RetrieveTopic < Jobs::Base | ||
|
|
||
| def execute(args) | ||
| raise Discourse::InvalidParameters.new(:embed_url) unless args[:embed_url].present? | ||
|
|
||
| user = nil | ||
| if args[:user_id] | ||
| user = User.where(id: args[:user_id]).first | ||
| end | ||
|
|
||
| TopicRetriever.new(args[:embed_url], no_throttle: user.try(:staff?)).retrieve | ||
| end | ||
|
|
||
| end | ||
|
|
||
| end | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,41 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Creates and Updates Topics based on an RSS or ATOM feed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| require 'digest/sha1' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| require_dependency 'post_creator' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| require_dependency 'post_revisor' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| require 'open-uri' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| module Jobs | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class PollFeed < Jobs::Scheduled | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recurrence { hourly } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sidekiq_options retry: false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def execute(args) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| poll_feed if SiteSetting.feed_polling_enabled? && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SiteSetting.feed_polling_url.present? && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SiteSetting.embed_by_username.present? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def feed_key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @feed_key ||= "feed-modified:#{Digest::SHA1.hexdigest(SiteSetting.feed_polling_url)}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def poll_feed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user = User.where(username_lower: SiteSetting.embed_by_username.downcase).first | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return if user.blank? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| require 'simple-rss' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rss = SimpleRSS.parse open(SiteSetting.feed_polling_url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical security vulnerability: Unsafe use of Using Replace with a safer HTTP client: - require 'simple-rss'
- rss = SimpleRSS.parse open(SiteSetting.feed_polling_url)
+ require 'simple-rss'
+ require 'net/http'
+
+ uri = URI.parse(SiteSetting.feed_polling_url)
+ raise "Invalid feed URL protocol" unless %w[http https].include?(uri.scheme)
+
+ response = Net::HTTP.get_response(uri)
+ raise "Failed to fetch feed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
+
+ rss = SimpleRSS.parse(response.body)
🧰 Tools🪛 Brakeman (7.0.2)[medium] 29-29: Model attribute used in file name (File Access) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rss.items.each do |i| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url = i.link | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url = i.id if url.blank? || url !~ /^https?\:\/\// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content = CGI.unescapeHTML(i.content.scrub) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TopicEmbed.import(user, url, i.title, content) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+24
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add comprehensive error handling for feed parsing. The Add proper error handling: def poll_feed
user = User.where(username_lower: SiteSetting.embed_by_username.downcase).first
return if user.blank?
+ begin
require 'simple-rss'
# ... (use safer HTTP client as suggested above)
rss.items.each do |i|
url = i.link
url = i.id if url.blank? || url !~ /^https?\:\/\//
+ next if url.blank?
content = CGI.unescapeHTML(i.content.scrub)
TopicEmbed.import(user, url, i.title, content)
end
+ rescue => e
+ Rails.logger.error "Feed polling failed: #{e.message}"
+ # Optionally notify administrators
+ end
end📝 Committable suggestion
Suggested change
🧰 Tools🪛 Brakeman (7.0.2)[medium] 29-29: Model attribute used in file name (File Access) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,82 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| require_dependency 'nokogiri' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class TopicEmbed < ActiveRecord::Base | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| belongs_to :topic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| belongs_to :post | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validates_presence_of :embed_url | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validates_presence_of :content_sha1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Import an article from a source (RSS/Atom/Other) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def self.import(user, url, title, contents) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return unless url =~ /^https?\:\/\// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contents << "\n<hr>\n<small>#{I18n.t('embed.imported_from', link: "<a href='#{url}'>#{url}</a>")}</small>\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| embed = TopicEmbed.where(embed_url: url).first | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content_sha1 = Digest::SHA1.hexdigest(contents) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| post = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # If there is no embed, create a topic, post and the embed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if embed.blank? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Topic.transaction do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| creator = PostCreator.new(user, title: title, raw: absolutize_urls(url, contents), skip_validations: true, cook_method: Post.cook_methods[:raw_html]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| post = creator.create | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if post.present? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TopicEmbed.create!(topic_id: post.topic_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| embed_url: url, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content_sha1: content_sha1, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| post_id: post.id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| post = embed.post | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Update the topic if it changed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if content_sha1 != embed.content_sha1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| revisor = PostRevisor.new(post) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| revisor.revise!(user, absolutize_urls(url, contents), skip_validations: true, bypass_rate_limiter: true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| embed.update_column(:content_sha1, content_sha1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| post | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Refactor complex method and address security concerns. This method has high complexity (ABC size 24.78/23) and mixes multiple responsibilities. Additionally, there are security concerns with HTML injection in the footer. Security Issue: The I18n interpolation includes raw HTML which could lead to XSS: -contents << "\n<hr>\n<small>#{I18n.t('embed.imported_from', link: "<a href='#{url}'>#{url}</a>")}</small>\n"
+safe_url = ERB::Util.html_escape(url)
+contents << "\n<hr>\n<small>#{I18n.t('embed.imported_from', link: "<a href='#{safe_url}'>#{safe_url}</a>")}</small>\n"Complexity Reduction: Consider extracting methods: def self.import(user, url, title, contents)
return unless valid_url?(url)
contents_with_footer = add_source_footer(url, contents)
content_sha1 = Digest::SHA1.hexdigest(contents_with_footer)
embed = find_embed(url)
if embed.blank?
create_new_embed(user, url, title, contents_with_footer, content_sha1)
else
update_existing_embed(user, embed, contents_with_footer, content_sha1)
end
end
private
def self.valid_url?(url)
url =~ /\Ahttps?:\/\//
end
def self.add_source_footer(url, contents)
safe_url = ERB::Util.html_escape(url)
contents + "\n<hr>\n<small>#{I18n.t('embed.imported_from', link: "<a href='#{safe_url}'>#{safe_url}</a>")}</small>\n"
end🧰 Tools🪛 RuboCop (1.76.1)[convention] 10-42: Assignment Branch Condition size for (Metrics/AbcSize) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def self.import_remote(user, url, opts=nil) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| require 'ruby-readability' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opts = opts || {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| doc = Readability::Document.new(open(url).read, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tags: %w[div p code pre h1 h2 h3 b em i strong a img], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attributes: %w[href src]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TopicEmbed.import(user, url, opts[:title] || doc.title, doc.content) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+44
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical security vulnerability: SSRF attack vector. The Replace with a safer HTTP client with proper restrictions: def self.import_remote(user, url, opts=nil)
require 'ruby-readability'
+ require 'net/http'
+ require 'uri'
opts = opts || {}
- doc = Readability::Document.new(open(url).read,
+
+ # Validate URL and fetch safely
+ uri = URI.parse(url)
+ raise ArgumentError, 'Only HTTP/HTTPS URLs allowed' unless %w[http https].include?(uri.scheme)
+ raise ArgumentError, 'Invalid host' if uri.host.nil?
+
+ # Prevent SSRF to internal networks
+ raise ArgumentError, 'Access to private networks not allowed' if private_network?(uri.host)
+
+ response = fetch_url_safely(uri)
+ doc = Readability::Document.new(response,
tags: %w[div p code pre h1 h2 h3 b em i strong a img],
attributes: %w[href src])
TopicEmbed.import(user, url, opts[:title] || doc.title, doc.content)
end
+private
+
+def self.private_network?(host)
+ # Add logic to check for private IP ranges, localhost, etc.
+ %w[localhost 127.0.0.1 ::1].include?(host) ||
+ host.match(/\A(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/)
+end
+
+def self.fetch_url_safely(uri)
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
+ open_timeout: 10, read_timeout: 30) do |http|
+ request = Net::HTTP::Get.new(uri)
+ response = http.request(request)
+ raise "HTTP error: #{response.code}" unless response.code == '200'
+ response.body
+ end
+end📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Convert any relative URLs to absolute. RSS is annoying for this. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def self.absolutize_urls(url, contents) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uri = URI(url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prefix = "#{uri.scheme}://#{uri.host}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prefix << ":#{uri.port}" if uri.port != 80 && uri.port != 443 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fragment = Nokogiri::HTML.fragment(contents) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fragment.css('a').each do |a| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| href = a['href'] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if href.present? && href.start_with?('/') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| a['href'] = "#{prefix}/#{href.sub(/^\/+/, '')}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fragment.css('img').each do |a| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| src = a['src'] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if src.present? && src.start_with?('/') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| a['src'] = "#{prefix}/#{src.sub(/^\/+/, '')}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fragment.to_html | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def self.topic_id_for_embed(embed_url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TopicEmbed.where(embed_url: embed_url).pluck(:topic_id).first | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,30 @@ | ||||||||||||||||||
| <header> | ||||||||||||||||||
| <%- if @topic_view.posts.present? %> | ||||||||||||||||||
| <%= link_to(I18n.t('embed.title'), @topic_view.topic.url, class: 'button', target: '_blank') %> | ||||||||||||||||||
| <%- else %> | ||||||||||||||||||
| <%= link_to(I18n.t('embed.start_discussion'), @topic_view.topic.url, class: 'button', target: '_blank') %> | ||||||||||||||||||
| <%- end if %> | ||||||||||||||||||
|
Comment on lines
+3
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add security attributes to external links. Links that open in new tabs should include Apply this diff: - <%= link_to(I18n.t('embed.title'), @topic_view.topic.url, class: 'button', target: '_blank') %>
+ <%= link_to(I18n.t('embed.title'), @topic_view.topic.url, class: 'button', target: '_blank', rel: 'noopener') %>
<%- else %>
- <%= link_to(I18n.t('embed.start_discussion'), @topic_view.topic.url, class: 'button', target: '_blank') %>
+ <%= link_to(I18n.t('embed.start_discussion'), @topic_view.topic.url, class: 'button', target: '_blank', rel: 'noopener') %>Also apply the same fix to lines 14, 25, and 26. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| <%= link_to(image_tag(SiteSetting.logo_url, class: 'logo'), Discourse.base_url) %> | ||||||||||||||||||
| </header> | ||||||||||||||||||
|
|
||||||||||||||||||
| <%- if @topic_view.posts.present? %> | ||||||||||||||||||
| <%- @topic_view.posts.each do |post| %> | ||||||||||||||||||
| <article class='post'> | ||||||||||||||||||
| <%= link_to post.created_at.strftime("%e %b %Y"), post.url, class: 'post-date', target: "_blank" %> | ||||||||||||||||||
| <div class='author'> | ||||||||||||||||||
| <img src='<%= post.user.small_avatar_url %>'> | ||||||||||||||||||
| <h3><%= post.user.username %></h3> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| <div class='cooked'><%= raw post.cooked %></div> | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainSecurity concern: Raw HTML rendering without sanitization. The Verify that the Let me check how the post content is processed: 🏁 Script executed: #!/bin/bash
# Check how post.cooked content is processed and if it's sanitized
ast-grep --pattern 'def cook($$$) { $$$ }'
rg -A 10 "cook.*raw_html"
rg -A 5 "TopicEmbed.*import"Length of output: 5095 Action Required: Sanitize embedded HTML and harden link‐security It turns out that • app/views/embed/best.html.erb (line 19)
<%= l(post.created_at, format: :short) %>🤖 Prompt for AI Agents |
||||||||||||||||||
| <div style='clear: both'></div> | ||||||||||||||||||
| </article> | ||||||||||||||||||
| <%- end %> | ||||||||||||||||||
|
|
||||||||||||||||||
| <footer> | ||||||||||||||||||
| <%= link_to(I18n.t('embed.continue'), @topic_view.topic.url, class: 'button', target: '_blank') %> | ||||||||||||||||||
| <%= link_to(image_tag(SiteSetting.logo_url, class: 'logo'), Discourse.base_url) %> | ||||||||||||||||||
| </footer> | ||||||||||||||||||
|
|
||||||||||||||||||
| <% end %> | ||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add error handling for DOM manipulation.
The code assumes
discourse-commentselement exists and doesn't handle cases where it might be missing.Add defensive programming:
var comments = document.getElementById('discourse-comments'), iframe = document.createElement('iframe'); + + if (!comments) { + console.error('Element with id "discourse-comments" not found'); + return; + } + iframe.src = discourseUrl + "embed/best?embed_url=" + encodeURIComponent(discourseEmbedUrl); iframe.id = 'discourse-embed-frame'; iframe.width = "100%"; iframe.frameBorder = "0"; iframe.scrolling = "no"; comments.appendChild(iframe);📝 Committable suggestion
🤖 Prompt for AI Agents