SECURITY: XSS on chat excerpts
Non-markdown tags weren't being escaped in chat excerpts. This could be triggered by editing a chat message containing a tag (self XSS), or by replying to a chat message with a tag (XSS). Co-authored-by: Jan Cernik <jancernik12@gmail.com>
This commit is contained in:
parent
fd16eade7f
commit
a373bf2a01
|
@ -30,7 +30,7 @@
|
||||||
/>
|
/>
|
||||||
<TopicStatus @topic={{t}} @disableActions={{true}} />
|
<TopicStatus @topic={{t}} @disableActions={{true}} />
|
||||||
<span class="topic-title">
|
<span class="topic-title">
|
||||||
{{replace-emoji t.fancy_title}}
|
{{replace-emoji t.title}}
|
||||||
</span>
|
</span>
|
||||||
<span class="topic-categories">
|
<span class="topic-categories">
|
||||||
{{bound-category-link
|
{{bound-category-link
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { emojiUnescape } from "discourse/lib/text";
|
import { emojiUnescape } from "discourse/lib/text";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe, isHTMLSafe } from "@ember/template";
|
||||||
import { registerUnbound } from "discourse-common/lib/helpers";
|
import { registerUnbound } from "discourse-common/lib/helpers";
|
||||||
|
import { escapeExpression } from "discourse/lib/utilities";
|
||||||
|
|
||||||
registerUnbound("replace-emoji", (text, options) => {
|
registerUnbound("replace-emoji", (text, options) => {
|
||||||
|
text = isHTMLSafe(text) ? text.toString() : escapeExpression(text);
|
||||||
return htmlSafe(emojiUnescape(text, options));
|
return htmlSafe(emojiUnescape(text, options));
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
data-title={{result.fancy_title}}
|
data-title={{result.fancy_title}}
|
||||||
>
|
>
|
||||||
<TopicStatus @topic={{result}} @disableActions={{true}} />
|
<TopicStatus @topic={{result}} @disableActions={{true}} />
|
||||||
{{replace-emoji result.fancy_title}}
|
{{replace-emoji result.title}}
|
||||||
<div class="search-category">
|
<div class="search-category">
|
||||||
{{#if result.category.parentCategory}}
|
{{#if result.category.parentCategory}}
|
||||||
{{category-link result.category.parentCategory}}
|
{{category-link result.category.parentCategory}}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
href={{rt.relative_url}}
|
href={{rt.relative_url}}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>{{replace-emoji rt.fancy_title}}</a>
|
>{{replace-emoji rt.title}}</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="reviewable-count">
|
<td class="reviewable-count">
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { module, test } from "qunit";
|
||||||
|
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||||
|
import { render } from "@ember/test-helpers";
|
||||||
|
import { hbs } from "ember-cli-htmlbars";
|
||||||
|
|
||||||
|
module("Integration | Helper | replace-emoji", function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
test("it replaces the emoji", async function (assert) {
|
||||||
|
await render(hbs`<span>{{replace-emoji "some text :heart:"}}</span>`);
|
||||||
|
|
||||||
|
assert.dom(`span`).includesText("some text");
|
||||||
|
assert.dom(`.emoji[title="heart"]`).exists();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it escapes the text", async function (assert) {
|
||||||
|
await render(
|
||||||
|
hbs`<span>{{replace-emoji "<style>body: {background: red;}</style>"}}</span>`
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.dom(`span`).hasText("<style>body: {background: red;}</style>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it renders html-safe text", async function (assert) {
|
||||||
|
await render(hbs`<span>{{replace-emoji (html-safe "safe text")}}</span>`);
|
||||||
|
|
||||||
|
assert.dom(`span`).hasText("safe text");
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
<TopicStatus @topic={{this.item}} @disableActions={{true}} />
|
<TopicStatus @topic={{this.item}} @disableActions={{true}} />
|
||||||
<div class="topic-title">{{replace-emoji this.item.fancy_title}}</div>
|
<div class="topic-title">{{replace-emoji this.item.title}}</div>
|
||||||
<div class="topic-categories">
|
<div class="topic-categories">
|
||||||
{{bound-category-link
|
{{bound-category-link
|
||||||
this.item.category
|
this.item.category
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
</label>
|
</label>
|
||||||
<div class="chat-form__control">
|
<div class="chat-form__control">
|
||||||
<div class="channel-info-about-view__name">
|
<div class="channel-info-about-view__name">
|
||||||
{{replace-emoji this.channel.escapedTitle}}
|
{{replace-emoji this.channel.title}}
|
||||||
</div>
|
</div>
|
||||||
<div class="channel-info-about-view__slug">
|
<div class="channel-info-about-view__slug">
|
||||||
{{this.channel.slug}}
|
{{this.channel.slug}}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
class="chat-channel-card__name-container"
|
class="chat-channel-card__name-container"
|
||||||
>
|
>
|
||||||
<span class="chat-channel-card__name">
|
<span class="chat-channel-card__name">
|
||||||
{{replace-emoji @channel.escapedTitle}}
|
{{replace-emoji @channel.title}}
|
||||||
</span>
|
</span>
|
||||||
{{#if @channel.chatable.read_restricted}}
|
{{#if @channel.chatable.read_restricted}}
|
||||||
{{d-icon "lock" class="chat-channel-card__read-restricted"}}
|
{{d-icon "lock" class="chat-channel-card__read-restricted"}}
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
|
|
||||||
{{#if @channel.description}}
|
{{#if @channel.description}}
|
||||||
<div class="chat-channel-card__description">
|
<div class="chat-channel-card__description">
|
||||||
{{replace-emoji @channel.escapedDescription}}
|
{{replace-emoji @channel.description}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</span>
|
</span>
|
||||||
<span class="chat-channel-title__name">
|
<span class="chat-channel-title__name">
|
||||||
{{replace-emoji this.channel.escapedTitle}}
|
{{replace-emoji this.channel.title}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{{#if (has-block)}}
|
{{#if (has-block)}}
|
||||||
|
|
|
@ -189,6 +189,34 @@ RSpec.describe "Chat channel", type: :system, js: true do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when replying to message that has tags" do
|
||||||
|
fab!(:other_user) { Fabricate(:user) }
|
||||||
|
fab!(:message_2) do
|
||||||
|
Fabricate(
|
||||||
|
:chat_message,
|
||||||
|
user: other_user,
|
||||||
|
chat_channel: channel_1,
|
||||||
|
message: "<mark>not marked</mark>",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:chat_message, user: other_user, chat_channel: channel_1)
|
||||||
|
Fabricate(:chat_message, in_reply_to: message_2, user: current_user, chat_channel: channel_1)
|
||||||
|
channel_1.add(other_user)
|
||||||
|
channel_1.add(current_user)
|
||||||
|
sign_in(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "escapes the reply-to line" do
|
||||||
|
chat.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(find(".chat-reply .chat-reply__excerpt")["innerHTML"].strip).to eq(
|
||||||
|
"<mark>not marked</mark>",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "when messages are separated by a day" do
|
context "when messages are separated by a day" do
|
||||||
before do
|
before do
|
||||||
Fabricate(:chat_message, chat_channel: channel_1, created_at: 2.days.ago)
|
Fabricate(:chat_message, chat_channel: channel_1, created_at: 2.days.ago)
|
||||||
|
|
Loading…
Reference in New Issue