Enhance embed URL handling and validation system
marchugon/discourse-cursor-fr-benchmark#8Golden Comments (6/6 found)
| # | Comment | Severity | Status | FriendlyReviewer Detail |
|---|---|---|---|---|
| #1 | SSRF via open(url).read in TopicEmbed.import_remote — user-supplied URL passed directly to open() without validation |
HIGH | ✓ Found | FriendlyReviewer: "Kernel#open risk: user-supplied URL may execute pipe commands. Use OpenURI.open_uri with explicit timeouts. Also missing error handling for network failures." (topic_embed.rb:40) |
| #2 | Origin validation bypass: discourseUrl.indexOf(e.origin) — malicious subdomain passes the check |
MEDIUM | ✓ Found | FriendlyReviewer: "Origin validation uses substring match via indexOf; attacker could craft origin that contains legitimate URL as substring. Use new URL(discourseUrl).origin for exact comparison." (embed.js:20) |
| #3 | postMessage targetOrigin: full referer URL passed instead of just the origin |
MEDIUM | ✓ Found | FriendlyReviewer: "targetOrigin receives full referer URL instead of just origin. Use URI(request.referer).origin to parse scheme+host+port." (embed.html.erb:10) |
| #4 | X-Frame-Options: ALLOWALL disables anti-clickjacking protection |
MEDIUM | ✓ Found | FriendlyReviewer: "Non-standard X-Frame-Options: ALLOWALL not recognized by modern browsers. Use Content-Security-Policy: frame-ancestors <embeddable_host> instead." (embed_controller.rb:28) |
| #5 | TopicEmbed.import: contents << ... mutates caller's string, and XSS via unescaped URL interpolation |
MEDIUM | ◈ Partial | FriendlyReviewer found the string mutation via << (L9) and the TOCTOU race condition between where / create! (L12). Missed NoMethodError on nil contents and XSS via <a href='#{url}'>#{url}</a>. |
| #6 | ERB syntax error: end if instead of end in best.html.erb |
MEDIUM | ✓ Found | FriendlyReviewer: "ERB syntax error: <%- end if %> is invalid. Change to <% end %>." (best.html.erb:6) |
Supplementary Findings (14 findings)
| Finding | File | Severity | Legitimate? |
|---|---|---|---|
XSS risk: cook_method == :raw_html returns raw unsanitized HTML as cooked |
post.rb:131 | HIGH | ✓ Stored XSS through bypass of all sanitization |
Missing require 'cgi' for CGI.unescapeHTML — relies on brittle Rails autoloading |
poll_feed.rb:37 | HIGH | ✓ Could fail in production depending on load order |
force: true added to existing migration will drop and recreate top_topics table |
top_topics.rb:3 | HIGH | ✓ Destroys existing data on migration |
Mock for TopicView incomplete — will crash during view rendering (lacks stubs for .posts, .topic, .url) |
embed_controller_spec.rb:49 | HIGH | ✓ Test would fail at runtime |
EditRateLimiter.performed! (Redis) called inside DB transaction — no rollback if transaction fails |
post_revisor.rb:49 | MEDIUM | ✓ Rate limit irreversibly consumed on failure |
Unchecked return value of @post.save — validation failure continues execution silently |
post_revisor.rb:83 | MEDIUM | ✓ Silent failure with side effects |
| No per-item error handling in poll loop — exception in one item aborts entire loop | poll_feed.rb:31 | MEDIUM | ✓ Single malformed feed blocks all subsequent feeds |
Missing foreign key constraints on topic_id and post_id |
topic_embeds.rb:4 | MEDIUM | ✓ Orphaned records possible |
| Infinite retry loop in loading template with no max limit | loading.html.erb:7 | MEDIUM | ✓ Browser resource exhaustion |
Assertions in after block are fragile — move into each it block |
embed_controller_spec.rb:35 | MEDIUM | ✓ Test reliability concern |
if (parent) is always truthy — should check window !== window.parent |
embed.html.erb:7 | LOW | ✓ Unnecessary postMessage when not embedded |
Orphaned translation key embed.permalink defined but never used |
server.en.yml:37 | LOW | ◈ Acceptable noise |
| Grammatical error: "a RSS/ATOM feed" should be "an RSS/ATOM feed" | server.en.yml:769 | LOW | ◈ Acceptable noise |
Missing as: name on new embed route — consider as: :embed_best |
routes.rb:244 | LOW | ◈ Acceptable noise |
Honest Analysis
FriendlyReviewer's full review
As posted on the PR — the complete output from FriendlyReviewer.
Hi there 👋
| 🌥️ Tech |
|---|
| 11 high, 6 medium, 4 low |
This merge request introduces an embed feature for Discourse, enabling external sites to display Discourse topics/comments in iframes, with RSS/Atom feed polling and automatic topic creation. The review identified several critical security, data integrity, and reliability issues that must be addressed before deployment.
Security: Multiple XSS vectors and origin validation weaknesses. The raw_html cook method bypasses all sanitization (stored XSS). postMessage origin validation uses substring matching, and targetOrigin passes full URL instead of origin. The X-Frame-Options header uses non-standard ALLOWALL. Kernel#open risks command injection.
Data Integrity: TopicEmbed.import mutates caller's string via <<. Idempotency race condition in embed creation. Missing FK constraints on topic_embeds and top_topics tables. Migration force: true on existing table will drop data.
Code Quality: ERB syntax error in best.html.erb. Missing require 'cgi' in poll_feed.rb. EditRateLimiter.performed! inside DB transaction without rollback. Unchecked save return value in PostRevisor. Missing trailing newlines.
Testing: Assertions in after block, incomplete mocks causing runtime failures, and significant coverage gaps (malformed referer, loading template, import_remote, idempotency).
Configuration: Orphaned translation key, grammatical error, and missing route name.
Recommendation: Address all high-criticality findings before merging. Many issues stem from insufficient input validation and error handling.
These might need a close look
- 🔴 app/models/post.rb (L131)
XSS risk:cook_method == :raw_htmlreturns raw unsanitized HTML ascooked. Run through an HTML sanitizer (e.g., Loofah) to strip malicious tags and attributes. - 🔴 app/controllers/embed_controller.rb (L28)
Non-standardX-Frame-Options: ALLOWALLnot recognized by modern browsers. UseContent-Security-Policy: frame-ancestors <embeddable_host>instead. - 🔴 app/assets/javascripts/embed.js (L20)
Origin validation uses substring matchindexOf; attacker could craft origin that contains legitimate URL as substring. Use exact origin comparison vianew URL(discourseUrl).origin. - 🔴 app/views/layouts/embed.html.erb (L10)
postMessagetargetOriginreceives full referer URL instead of just origin. UseURI(request.referer).originto parse scheme+host+port. - 🔴 app/models/topic_embed.rb (L9)
contents << "..."mutates caller's string. Usecontents + "..."to avoid corrupting feed content data. - 🔴 app/models/topic_embed.rb (L12)
TOCTOU race condition betweenwhereandcreate!. Wrap in rescue forActiveRecord::RecordNotUniqueor usefind_or_create_by. - 🔴 app/models/topic_embed.rb (L40)
Kernel#openrisk: user-supplied URL may execute pipe commands. UseOpenURI.open_uriwith explicit timeouts. Also missing error handling for network failures. - 🔴 app/jobs/scheduled/poll_feed.rb (L37)
Missingrequire 'cgi'forCGI.unescapeHTML. Add require to avoid brittle dependency on Rails autoloading. - 🔴 db/migrate/20131223171005_create_top_topics.rb (L3)
force: trueadded to existing migration will drop and recreatetop_topicstable, destroying existing data. Use new migration for schema changes. - 🔴 app/views/embed/best.html.erb (L6)
ERB syntax error:<%- end if %>is invalid. Change to<% end %>. - 🔴 spec/controllers/embed_controller_spec.rb (L49)
Mock for TopicView incomplete; will crash during view rendering as it lacks stubs for.posts,.topic,.url, etc. Use real object or full stub.
Worth checking
- 🟡 lib/post_revisor.rb (L49)
EditRateLimiter.performed!(Redis) called inside DB transaction. If transaction rolls back, rate limit is irreversibly consumed. Move outside transaction or add rollback. - 🟡 lib/post_revisor.rb (L83)
Unchecked return value of@post.save. Validation failure continues execution, consuming rate limit and potentially bumping topic. Add guard to return false on failure. - 🟡 app/jobs/scheduled/poll_feed.rb (L31)
No per-item error handling; exception in one item aborts entire loop. Wrap loop body in begin/rescue to isolate failures. - 🟡 db/migrate/20131217174004_create_topic_embeds.rb (L4)
Missing foreign key constraints ontopic_idandpost_id. Addforeign_key: trueto prevent orphaned records. - 🟡 app/views/embed/loading.html.erb (L7)
Infinite retry loop with no max limit. Implement max retries using sessionStorage or query parameter. - 🟡 spec/controllers/embed_controller_spec.rb (L35)
Assertions inafterblock are fragile; move into eachitblock.
Small things (take or leave)
- 🔵 app/views/layouts/embed.html.erb (L7)
if (parent)is always truthy; should checkwindow !== window.parentto avoid sending postMessage when not embedded. - 🔵 config/locales/server.en.yml (L37)
Orphaned translation keyembed.permalinkdefined but never used. Remove or wire to view. - 🔵 config/locales/server.en.yml (L769)
Grammatical error: "a RSS/ATOM feed" should be "an RSS/ATOM feed". - 🔵 config/routes.rb (L244)
Missingas:name on new embed route; consider addingas: :embed_bestfor consistency.
FriendlyReviewer detected 5 of the 6 golden comments in this PR, with the remaining golden comment partially identified — a strong result. The security coverage is particularly impressive: SSRF via
Kernel#open, origin validation bypass via substring matching,postMessagetargetOriginleaking the full referer URL, and non-standardX-Frame-Options: ALLOWALLwere all correctly diagnosed with precise remediation suggestions. These are four distinct security issues across two languages (Ruby and JavaScript), and FriendlyReviewer identified every one.Golden comment #5 (
TopicEmbed.importvulnerabilities) was only partially found. FriendlyReviewer correctly identified the string mutation via<<(L9) and the TOCTOU race condition betweenwhereandcreate!(L12). However, it missed theNoMethodErrorthat would occur whencontentsisnil(no guard before calling<<), and the stored XSS via unescaped URL interpolation in the generated HTML anchor tag. These two sub-bugs are more subtle — the nil case requires reasoning about the full call chain, and the XSS depends on understanding that#{url}is not HTML-escaped in the template context.Beyond the golden set, FriendlyReviewer uncovered several serious bugs. The
raw_htmlcook method inPostbypasses all sanitization and would allow stored XSS through any embedding flow — a critical finding that no golden comment covers. Theforce: trueflag on an existing migration would silently drop thetop_topicstable and destroy all production data on deploy. The missingrequire 'cgi'is a brittle dependency on Rails autoloading that could fail in production depending on load order. The incomplete mock forTopicViewwould cause a runtime crash during test execution, not just a false positive — the tests as written cannot pass.The medium-severity findings are also legitimate:
EditRateLimiter.performed!inside a DB transaction creates an irreversible Redis side effect if the transaction rolls back; the unchecked@post.savereturn value silently continues execution past a validation failure; the missing foreign key constraints ontopic_embedsallow orphaned records; and the infinite retry loop inloading.html.erbcould exhaust browser resources.A small amount of noise is present: the orphaned
embed.permalinktranslation key, the grammatical "a RSS" → "an RSS" error, and the missing routeas:name are all trivial. This level of noise is acceptable — it is the trade-off for a reviewer that casts a wide net and finds real bugs that no human benchmark would credit.