CVE-2021-25974 (Ruby, published 2021-11-10T11:15Z)
Description: value: In Publify, versions v8.0 to v9.2.4 are vulnerable to stored XSS. A user with a “publisher” role is able to inject and execute arbitrary JavaScript code while creating a page/article.
Severity: LOW (CVSS v3 base score: 5.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/publify/publify
Files changed:
publify_core/app/helpers/base_helper.rb
publify_core/app/models/content_base.rb
publify_core/app/models/feedback.rb
publify_core/app/views/articles/_article_excerpt.html.erb
publify_core/app/views/articles/_full_article_content.html.erb
publify_core/app/views/articles/view_page.html.erb
publify_core/app/views/comments/_comment.html.erb
publify_core/app/views/notes/_note.html.erb
publify_core/app/views/notes/index.html.erb
publify_core/spec/helpers/base_helper_spec.rb
publify_core/spec/models/article_spec.rb
publify_core/spec/models/comment_spec.rb
publify_core/spec/models/content_spec.rb
publify_core/spec/models/note_spec.rb
publify_core/spec/models/page_spec.rb
themes/bootstrap-2/views/articles/view_page.html.erb
themes/bootstrap-2/views/comments/_comment.html.erb
Commit message: Ensure all Content html is sanitized

- Require all content to be html_safe before displaying
- Sanitize all content html, not just Comments
- Make nofollowify_links take and return html_safe strings


Vulnerable code:
      string.gsub(/<a(.*?)>/i, '<a\1 rel="nofollow">')
  # Post-process the HTML.  This is a noop by default, but Comment overrides it
  # to enforce HTML sanity.
    html
  class ContentTextHelpers
    include ActionView::Helpers::UrlHelper
    include ActionView::Helpers::TextHelper
    include ActionView::Helpers::SanitizeHelper
  end

    <%= raw article.html(:body) %>
  <%= raw article.html(:body) %>
  <%= raw article.html(:extended) %>
  <%= raw html @page %>
      <%= raw nofollowify_links comment.generate_html(:body) %>
    <div class='p-name entry-title e-content entry-content article'><%= raw note.html(:body) %></div>
      <p class='p-name entry-title e-content entry-content article'><%= raw note.html(:body) %></p>
      expect(nofollowify_links('<a href="http://myblog.net">my blog</a>')).
        to eq('<a href="http://myblog.net" rel="nofollow">my blog</a>')
    it "with dofollowify enabled, links should be nofollowed" do
      expect(nofollowify_links('<a href="http://myblog.net">my blog</a>')).
        to eq('<a href="http://myblog.net">my blog</a>')
  describe "#generate_html" do
      expect(comment.generate_html(:body)).to match(/mailto:/)

  describe "#generate_html" do
    context "with a blog with markdown filter" do
      let!(:blog) { create(:blog, comment_text_filter: "markdown") }

      context "comment with italic and bold" do
        let(:comment) { build(:comment, body: "Comment body _italic_ **bold**") }

        it "converts the comment markup to HTML" do
          expect(comment.generate_html(:body)).to match(%r{<em>italic</em>})
          expect(comment.generate_html(:body)).to match(%r{<strong>bold</strong>})
        end
      end
    end
  end
<%= raw @page.html %>
    <%= raw comment.html %>

Fixed code:
    raise ArgumentError, "string", "must be html_safe" unless string.html_safe?

      followify_scrubber = Loofah::Scrubber.new do |node|
        node.set_attribute "rel", "nofollow" if node.name == "a"
      end
      sanitize h(string), scrubber: followify_scrubber
  class ContentTextHelpers
    include ActionView::Helpers::UrlHelper
    include ActionView::Helpers::TextHelper
    include ActionView::Helpers::SanitizeHelper
  end

  # Post-process the HTML
    helper = ContentTextHelpers.new
    helper.sanitize html
    <%= article.html(:body) %>
  <%= article.html(:body) %>
  <%= article.html(:extended) %>
  <%= html @page %>
      <%= nofollowify_links comment.generate_html(:body) %>
    <div class='p-name entry-title e-content entry-content article'><%= note.html(:body) %></div>
      <p class='p-name entry-title e-content entry-content article'><%= note.html(:body) %></p>
    let(:original_html) { '<a href="http://myblog.net">my blog</a>'.html_safe }

      result = nofollowify_links(original_html)

      aggregate_failures do
        expect(result).to eq('<a href="http://myblog.net" rel="nofollow">my blog</a>')
        expect(result).to be_html_safe
      end
    it "with dofollowify enabled, links should be not nofollowed" do
      result = nofollowify_links(original_html)

      aggregate_failures do
        expect(result).to eq('<a href="http://myblog.net">my blog</a>')
        expect(result).to be_html_safe
      end
    end

    it "does not accept unsafe html" do
      expect { nofollowify_links("just an unsafe string") }.to raise_error ArgumentError
  describe "#html" do
    let(:article) { build_stubbed :article }

    it "returns an html_safe string" do
      expect(article.html).to be_html_safe
    end
  end

  describe "#html" do
      expect(comment.html).to match(/mailto:/)
    end

    it "returns an html_safe string" do
      comment = build_stubbed(:comment, body: "Just a comment")
      expect(comment.html).to be_html_safe
    end

    context "with an evil comment" do
      let(:comment) { build_stubbed :comment, body: "Test foo <script>do_evil();</script>" }
      let(:blog) { comment.article.blog }

      ["", "textile", "markdown", "smartypants", "markdown smartypants"].each do |filter|
        it "rejects xss attempt with filter '#{filter}'" do
          blog.comment_text_filter = filter

          ActiveSupport::Deprecation.silence do
            assert comment.html(:body) !~ /<script>/
          end
        end
      end
    end

    context "with a markdown comment with italic and bold" do
      let(:comment) { build(:comment, body: "Comment body _italic_ **bold**") }
      let(:blog) { comment.article.blog }

      it "converts the comment markup to html" do
        blog.comment_text_filter = "markdown"
        result = comment.html

        aggregate_failures do
          expect(result).to match(%r{<em>italic</em>})
          expect(result).to match(%r{<strong>bold</strong>})
        end
      end

    describe "#html" do
      it "returns an html_safe string" do
        note = build(:note, body: "A test tweet with a #hashtag")
        expect(note.html).to be_html_safe
      end
    end

  describe "#html" do
    it "returns an html_safe string" do
      page = build(:page)
      expect(page.html).to be_html_safe
    end
  end
<%= @page.html %>
    <%= comment.html %>