Enhance embed URL handling and validation system#7
Conversation
Codoki PR ReviewSummary: Remove unsafe postMessage origin, prevent data loss Issues (Critical & High only)
Showing top 5 issues. Critical: 4, High: 4. See inline suggestions for more. Key Feedback (click to expand)
Confidence: 1/5 — Unsafe to merge (4 critical · 4 high · status: Requires changes · scope: large PR (28 files)) Sequence DiagramsequenceDiagram
participant Parent
participant Embed
Parent->>Embed: load iframe
Embed->>Parent: postMessage({type: 'discourse-resize', height: document.body.offsetHeight}, origin)
React with 👍 or 👎 if you found this review useful. |
| # 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]) |
There was a problem hiding this comment.
🛑 Critical: Security: Untrusted HTML is being persisted and later rendered with 'raw' by using cook_method :raw_html and skipping validations. This allows XSS (e.g., <img src=x onerror=alert(1)> from a feed item will execute in the embed). Additionally, earlier in this method the appended footer interpolates url directly into an href attribute without escaping, enabling attribute injection if the URL contains quotes. Please sanitize imported HTML and escape the appended URL. At minimum, avoid :raw_html here and let the standard cooking/sanitization pipeline run.
| creator = PostCreator.new(user, title: title, raw: absolutize_urls(url, contents), skip_validations: true, cook_method: Post.cook_methods[:raw_html]) | |
| creator = PostCreator.new(user, title: title, raw: absolutize_urls(url, contents), skip_validations: true, cook_method: Post.cook_methods[:regular]) |
| 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) |
There was a problem hiding this comment.
:retrieve_topic that isn't defined in this PR, and the spec asserts that TopicRetriever is instantiated and retrieve is called. This will break tests and the feature at runtime. Delegate to TopicRetriever here (and if you later add a job, have it call the same retriever to keep guards consistent).
| Jobs.enqueue(:retrieve_topic, user_id: current_user.try(:id), embed_url: embed_url) | |
| TopicRetriever.new(embed_url).retrieve |
| <%= 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 %> |
There was a problem hiding this comment.
🛑 Critical: ERB syntax error: end if is invalid and will crash template rendering. Close the conditional with a plain end.
| <%- end if %> | |
| <%- end %> |
|
|
||
| def cook(*args) | ||
| # For some posts, for example those imported via RSS, we support raw HTML. In that | ||
| # case we can skip the rendering pipeline. |
There was a problem hiding this comment.
🛑 Critical: This returns raw HTML directly and bypasses the normal sanitization pipeline, which opens an XSS vector if the content originates from remote feeds or untrusted sources (e.g., '<script>alert(1)</script>' would be rendered as-is). At minimum, sanitize the HTML before returning, or route it through a sanitizer that matches Discourse's allowlist.
| # case we can skip the rendering pipeline. | |
| return Sanitize.fragment(raw, Sanitize::Config::RELAXED) if cook_method == Post.cook_methods[:raw_html] |
| user = User.where(id: args[:user_id]).first | ||
| end | ||
|
|
||
| TopicRetriever.new(args[:embed_url], no_throttle: user.try(:staff?)).retrieve |
There was a problem hiding this comment.
| TopicRetriever.new(args[:embed_url], no_throttle: user.try(:staff?)).retrieve | |
| uri = URI.parse(args[:embed_url]) rescue nil | |
| raise Discourse::InvalidParameters.new(:embed_url) unless uri && uri.host && SiteSetting.embeddable_host.present? && (uri.host == SiteSetting.embeddable_host || uri.host.end_with?(".#{SiteSetting.embeddable_host}")) | |
| TopicRetriever.new(args[:embed_url], no_throttle: user.try(:staff?)).retrieve |
|
|
||
| function postMessageReceived(e) { | ||
| if (!e) { return; } | ||
| if (discourseUrl.indexOf(e.origin) === -1) { return; } |
There was a problem hiding this comment.
🔷 Medium: The origin check is reversed; it should verify that e.origin starts with the configured discourseUrl, otherwise a crafty partial match could pass or legitimate origins with trailing slashes/ports could fail.
| if (discourseUrl.indexOf(e.origin) === -1) { return; } | |
| if (!e.origin || e.origin.indexOf(discourseUrl.replace(/\/$/, '')) !== 0) { return; } |
| window.onload = function() { | ||
| if (parent) { | ||
| // Send a post message with our loaded height | ||
| parent.postMessage({type: 'discourse-resize', height: document['body'].offsetHeight}, '<%= request.referer %>'); |
There was a problem hiding this comment.
🛑 Critical: Security: request.referer is user-controlled and is embedded into a JS string without JS-context escaping, enabling XSS via crafted Referer. Additionally, postMessage targetOrigin should be a trusted origin (scheme+host+port), not a full URL, and referer can be nil causing runtime errors. Parse and restrict to a known/whitelisted origin and JSON-escape it before embedding.
| parent.postMessage({type: 'discourse-resize', height: document['body'].offsetHeight}, '<%= request.referer %>'); | |
| parent.postMessage({type: 'discourse-resize', height: document.body.offsetHeight}, <%= (begin uri = URI.parse(request.referer.to_s); "#{uri.scheme}://#{uri.host}#{(uri.port && ![80,443].include?(uri.port)) ? ":#{uri.port}" : ""}" rescue "*").to_json %>); |
| @@ -0,0 +1,13 @@ | |||
| class CreateTopicEmbeds < ActiveRecord::Migration | |||
| def change | |||
| create_table :topic_embeds, force: true do |t| | |||
There was a problem hiding this comment.
| create_table :topic_embeds, force: true do |t| | |
| create_table :topic_embeds do |t| |
| class CreateTopTopics < ActiveRecord::Migration | ||
| def change | ||
| create_table :top_topics do |t| | ||
| create_table :top_topics, force: true do |t| |
There was a problem hiding this comment.
| create_table :top_topics, force: true do |t| | |
| create_table :top_topics do |t| |
No description provided.