I was checking for some Discourse vulnerabilities, and I saw that a new CVE was dropped on 11/10/2023
CVE-2023-47119
The details didn’t mention any POC, so I did some analysis based on the source code commits changes to understand the vulnerability and how it is possible to exploit it.
The article includes details, lab setup and a demo.
A GitHub repository was created for the POC https://github.com/BaadMaro/CVE-2023-47119 Feel free to contribute with reports, escalations, and links to other POCs too.
CVE-2023-47119
CVE-2023-47119 is a new Discourse vulnerability affecting versions prior to version 3.1.3 of the stable
branch and version 3.2.0.beta3 of the beta
and tests-passed
branches. Some links can inject arbitrary HTML tags when rendered through the Onebox engine.
The severity is Medium 5.3
which is understandable as the vulnerability is only HTML injection and it needs a bypass for the XSS filter used by Discourse to cause a bigger impact.
Checking the CVE details CVE-2023-47119, we can see the commits added for the fix for example this one : https://github.com/discourse/discourse/commit/628b293ff53fb617b3464dd27268aec84388cc09
The interesting part is the fix for the github_issue_onebox.rb
file which reveals our target.
As we can see :
- The bug is affecting
/lib/onebox/engine/github_issue_onebox.rb
- An escape was added to the value of GitHub issue label.
- If we check the details of the file, we can see an emoji converter that converts the emoji code to an image. The code is described in
/app/helpers/emoji_helper.rb
which is a call for/app/models/emoji.rb
1
2
3
4
5
module EmojiHelper
def emoji_codes_to_img(str)
raw(Emoji.codes_to_img(str))
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def self.codes_to_img(str)
return if str.blank?
str =
str.gsub(/:([\w\-+]*(?::t\d)?):/) do |name|
code = $1
if code && Emoji.custom?(code)
emoji = Emoji[code]
"<img src=\"#{emoji.url}\" title=\"#{code}\" class=\"emoji\" alt=\"#{code}\" loading=\"lazy\" width=\"20\" height=\"20\">"
elsif code && Emoji.exists?(code)
"<img src=\"#{Emoji.url_for(code)}\" title=\"#{code}\" class=\"emoji\" alt=\"#{code}\" loading=\"lazy\" width=\"20\" height=\"20\">"
else
name
end
end
The function returns an image HTML element for known emojis, and if it doesn’t exist, it returns the same text used.
In /lib/onebox/engine/github_issue_onebox.rb
before the fix, we can see the emoji function is used in label part :
1
labels = raw["labels"].map { |l| { name: Emoji.codes_to_img(l["name"]) } }
The label is an identifier used by Github issues to specify, for example the type of issue.
In Discourse, the onboxe engine used for topic details and replies have a custom engine for GitHub issues which pulls the issue details via URL and converts them to a better view.
Lab setup
To build Discourse 3.1.3 which is a vulnerable version, I used the docker compose file by bitnami https://hub.docker.com/r/bitnami/discourse/
- docker-compose.yml : https://raw.githubusercontent.com/bitnami/containers/main/bitnami/discourse/docker-compose.yml
- Modify the 2 images tag to 3.1.3 or any other vulnerable version
docker.io/bitnami/discourse:3.1.3
- Change host to your preferred config like 0.0.0.0 or your internal network IP address
DISCOURSE_HOST=0.0.0.0
- You can also modify the port 80
- After modifying the file, run
docker-compose up -d
- Few minutes you’ll be able to see the discourse web server at your host port 80
- App default login
user:bitnami123
You can also use the official docker https://github.com/discourse/discourse_docker
Demo
To control the label name, we can create a repository with an issue and modify the label name assigned to the issue.
Example
In this example, the issue label was “bug”. We can now try including an emoji like :smile:
1
2
3
<span style="display:inline-block;margin-top:2px;background-color: #B8B8B8;padding: 2px;border-radius: 4px;color: #fff;margin-left: 3px;">
bug <img src="/images/emoji/twitter/smile.png?v=12" title="smile" class="emoji" alt="smile" width="20" height="20">
</span>
As we can see, it’s the same discussed output from the emoji function.
Now let’s confirm a no-existing emoji
1
2
3
<span style="display:inline-block;margin-top:2px;background-color: #B8B8B8;padding: 2px;border-radius: 4px;color: #fff;margin-left: 3px;">
bug :baadmaroemoji:
</span>
As the emoji function didn’t find the emoji, it returned the original text.
The returned text is not sanitized (emoji format or just simple text) which is the cause of the CVE.
We can confirm by injecting an h1 tag for example. Having the double “:” in label name is not necessary
1
2
3
<span style="display:inline-block;margin-top:2px;background-color: #B8B8B8;padding: 2px;border-radius: 4px;color: #fff;margin-left: 3px;">
bug <h1>BaadMaro HTML Injection POC</h1>
</span>
XSS Filters
https://github.com/discourse/discourse/blob/main/docs/SECURITY.md#xss
Discourse is using some mechanisms to protect against XSS :
- Node module
xss
https://jsxss.com/en/index.html - Server side allow list sanitizer using the Sanitize gem. See the relevant Discourse code.
- Titles and all other places where non-admins can enter code are protected either using the Handlebars library or standard Rails XSS protection.
- CSP
So to be able to escalate the CVE from HTML injection to XSS, you need a bypass for the used filters.
Conclusion
A GitHub repository was created for the POC : https://github.com/BaadMaro/CVE-2023-47119
You can contribute to the repository with reports, escalations and links to other POCs too.
Thank you.