CVE-2022-31059 (JavaScript, published 2022-06-14T20:15Z)
Description: value: Discourse Calendar is a calendar plugin for Discourse, an open-source messaging app. Prior to version 1.0.1, parsing and rendering of Event names can be susceptible to cross-site scripting (XSS) attacks. This vulnerability only affects sites which have modified or disabled Discourse’s default Content Security Policy. This issue is patched in version 1.0.1 of the Discourse Calendar plugin. As a workaround, ensure that the Content Security Policy is enabled, and has not been modified in a way which would make it more vulnerable to XSS attacks.
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/discourse/discourse-calendar
Files changed:
assets/javascripts/discourse/widgets/discourse-post-event.js
assets/javascripts/discourse/widgets/event-invitation-notification-item.js
assets/javascripts/discourse/widgets/event-reminder-notification-item.js
db/post_migrate/20220613073844_unescape_event_name.rb
lib/discourse_post_event/event_parser.rb
spec/lib/discourse_post_event/event_parser_spec.rb
Commit message: SECURITY: correctly escape event name (#280)



Vulnerable code:
        eventModel.name ||
import { formatUsername } from "discourse/lib/utilities";
        description = `<span data-topic-id="${this.attrs.topic_id}">${data.topic_title}</span>`;
import { formatUsername } from "discourse/lib/utilities";
      description = `<span data-topic-id="${this.attrs.topic_id}">${data.topic_title}</span>`;
            event[name.sub('data-', '').to_sym] = CGI.escapeHTML(value)

Fixed code:
import { escapeExpression } from "discourse/lib/utilities";
        escapeExpression(eventModel.name) ||
    topicTitle = escapeExpression(topicTitle);
import { escapeExpression, formatUsername } from "discourse/lib/utilities";
        description = `<span data-topic-id="${
          this.attrs.topic_id
        }">${escapeExpression(data.topic_title)}</span>`;
import { escapeExpression, formatUsername } from "discourse/lib/utilities";
      description = `<span data-topic-id="${
        this.attrs.topic_id
      }">${escapeExpression(data.topic_title)}</span>`;
# frozen_string_literal: true

class UnescapeEventName < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def up
    # event notifications
    start = 1
    limit = DB.query_single("SELECT MAX(id) FROM notifications WHERE notification_type IN (27, 28)").first.to_i

    notifications_query = <<~SQL
      SELECT id, data
      FROM notifications
      WHERE
        id >= :start AND
        notification_type IN (27, 28) AND
        data::json ->> 'topic_title' LIKE '%&%'
      ORDER BY id ASC
      LIMIT 1000
    SQL
    while true
      if start > limit
        break
      end
      max_seen = -1
      DB.query(notifications_query, start: start).each do |record|
        id = record.id
        if id > max_seen
          max_seen = id
        end
        data = JSON.parse(record.data)
        unescaped = CGI.unescapeHTML(data["topic_title"])
        next if unescaped == data["topic_title"]
        data["topic_title"] = unescaped
        DB.exec(<<~SQL, data: data.to_json, id: id)
          UPDATE notifications SET data = :data WHERE id = :id
        SQL
      end
      start += 1000
      if max_seen > start
        start = max_seen + 1
      end
    end

    # event names
    events_query = <<~SQL
      SELECT id, name
      FROM discourse_post_event_events
      WHERE name LIKE '%&%'
      ORDER BY id ASC
    SQL

    DB.query(events_query).each do |event|
      unescaped_name = CGI.unescapeHTML(event.name)
      next if unescaped_name == event.name
      DB.exec(<<~SQL, unescaped_name: unescaped_name, id: event.id)
        UPDATE discourse_post_event_events SET name = :unescaped_name WHERE id = :id
      SQL
    end
  end

  def down
    raise ActiveRecord::IrreversibleMigration
  end
end
            event[name.sub('data-', '').to_sym] = if name == "data-name"
              value
            else
              CGI.escapeHTML(value)
            end
  it 'doesn’t escape event name' do
    events = subject.extract_events(build_post(user, '[event start="foo" name="bar <script> baz"]\n[/event]'))
    expect(events[0][:name]).to eq("bar <script> baz")
  end