Renovate reads its configuration from the repositories it updates, and that configuration can pull in shared presets from other repositories, npm packages or any HTTP URL. Two advisories published on 29 September 2026, and given CVE IDs on 14 October, show what that means when the presets aren't trustworthy. CVE-2026-110867 let a preset set environment variables that the administrator had deliberately blocked, including PATH, LD_PRELOAD and NODE_OPTIONS, for the commands Renovate runs. CVE-2026-110868 let a preset or custom datasource make Renovate's HTTP client fetch internal URLs, such as a cloud metadata service that hands out credentials.
Both affect self-hosted Renovate, including Mend's Community and Enterprise editions. This post is for platform teams who run Renovate themselves. It explains both bugs, which versions fix them, the setting that's still at a permissive default after the fix, and a script that audits repository configs for the inputs these bugs relied on.
The short answer
Upgrade self-hosted Renovate to 44.79.0 or later (44.118.1 or later if your Git platform resolves to a private IP), or Mend Renovate CE/EE 15.6.0 or later. Then set internalHostAccess to block with explicit hostRules for the internal hosts Renovate needs, because the default is still warn, which permits internal requests and only logs them. Keep allowedEnv as narrow as possible, and review which external presets your repositories extend.
| CVE-2026-110867 | CVE-2026-110868 | |
|---|---|---|
| What | env in a shared preset bypassed the allowedEnv allowlist | SSRF through HTTP presets and customDatasources |
| Advisory | GHSA-6873-xr44-22vm (critical) | GHSA-8rqp-w6vc-pf27 (high) |
| Renovate fixed in | 44.60.1 (3 September 2026) | 44.79.0 (10 September 2026); 44.118.1 for platforms on private IPs |
| Mend Renovate CE / EE fixed in | 15.5.0 (EE Helm chart 10.5.0) | 15.6.0 (EE Helm chart 10.6.0) |
| Scores (VulnCheck CNA) | CVSS 4.0 9.0, CVSS 3.1 8.5 | CVSS 4.0 8.4, CVSS 3.1 8.5 |
| Reported by | jamietanna | offset and pvaladares |
CVE-2026-110867: a preset could set PATH
When Renovate runs package managers and other tools, it passes child processes only an allowlisted set of environment variables. Self-hosted administrators decide which variables repositories may set through allowedEnv, which defaults to an empty list. A repository that needs, say, GOPRIVATE can only set it if the administrator allows that name.
The advisory says the check could be bypassed by putting env inside a shared preset instead of the repository's own config. Presets can live in another repository, an npm package or on the public internet, so a preset outside your control could set PATH, LD_PRELOAD, NODE_OPTIONS or GIT_SSL_CAINFO for the tools Renovate runs. Any of those is enough to change what code runs or which TLS certificates are trusted. The advisory notes a targeted attack would need more information, but calls it a significant risk. It also says there's no workaround short of upgrading.
Renovate's documentation now states the rule plainly: allowedEnv "constrains what a repository, and the presets it extends, may set". Variables set by presets that the administrator chose in the global config still apply, because the administrator chose them.
CVE-2026-110868: presets as an SSRF channel
Renovate uses one shared HTTP client for registry lookups, HTTP presets and user-defined custom datasources. Before 44.79.0, a repository could point any of those at an arbitrary URL. The advisory's example extends a preset at the EC2 instance metadata endpoint, http://169.254.169.254/latest/meta-data/iam/security-credentials/, to try to reach the instance's AWS credentials. Responses could also leak through Renovate's logs, and requests could carry credentials.
The fix added a policy for internal hosts, controlled by a new setting, internalHostAccess:
warn, the default: requests to internal hosts are permitted, and each one thatblockwould refuse is logged as a warning.block: requests to internal hosts are refused unless the host is your platform endpoint or permitted by ahostRulesentry in your own configuration.allow: permitted and not logged, described as a temporary migration escape hatch.
After the fix, cloud metadata endpoints such as 169.254.169.254 and metadata.google.internal are refused in every mode, because, as the documentation explains, Renovate's HTTP layer never has a legitimate reason to request them. Under the default warn, requests to other internal hosts, including HTTP presets and custom datasources, still go through and are only logged. Under block, they need a hostRules entry, and presets fetched from an internal host need a scoped allowInternal grant. The project says the default will become block in a future major release and recommends setting block now, once the warnings show which hosts you need to permit. If your Git platform itself is on a private IP, the advisory recommends 44.118.1 or later, or an explicit hostRules[].allowInternal grant for the platform.
Audit your repositories' Renovate configs
Upgrading closes both bugs. It's still worth knowing which repositories extend presets from outside your organisation, fetch presets over HTTP, set environment variables, or point custom datasources at internal addresses, because those are the paths these bugs used and the places a future bug would use too. This script reads Renovate config files in each repository you give it, including package.json's renovate key, and reports them. It resolves hostnames to check whether they're internal, and needs the json5 package for .json5 configs, which many projects use.
#!/usr/bin/env python3
"""Audit repository Renovate configs for the inputs behind CVE-2026-110867 and CVE-2026-110868:
presets fetched from outside your organisation, HTTP presets, env overrides, and custom
datasources pointing at internal or metadata addresses. Reads files and resolves hostnames; needs `pip install json5` for .json5 configs.
Usage: python3 renovate_preset_audit.py --org my-github-org REPO_DIR [REPO_DIR ...]"""
import ipaddress, json, os, re, socket, sys
from urllib.parse import urlparse
CONFIG_NAMES = ("renovate.json", "renovate.json5", ".renovaterc", ".renovaterc.json", ".renovaterc.json5",
".github/renovate.json", ".github/renovate.json5", ".gitlab/renovate.json", ".gitlab/renovate.json5")
def load(path):
text = open(path).read()
if path.endswith("json5") or os.path.basename(path) == ".renovaterc":
import json5 # pip install json5; Renovate allows JSON5 here
cfg = json5.loads(text)
else:
cfg = json.loads(text)
return cfg.get("renovate", {}) if os.path.basename(path) == "package.json" else cfg
def internal(host):
try:
ip = ipaddress.ip_address(host)
except ValueError:
try:
ip = ipaddress.ip_address(socket.gethostbyname(host))
except (OSError, ValueError):
return False
return ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved
def walk(node):
"""Every dict nested anywhere in the config, including packageRules entries."""
if isinstance(node, dict):
yield node
for v in node.values():
yield from walk(v)
elif isinstance(node, list):
for v in node:
yield from walk(v)
def audit(cfg, org):
for d in walk(cfg):
for preset in d.get("extends", []) if isinstance(d.get("extends"), list) else []:
if preset.startswith(("http://", "https://")):
host = urlparse(preset).hostname or ""
yield ("HIGH" if internal(host) else "REVIEW"), f"HTTP preset {preset}" + (" (internal address)" if internal(host) else "")
elif preset.startswith(("github>", "gitlab>", "gitea>", "bitbucket>", "forgejo>", "npm:", "local>")):
owner = re.split(r"[>/:]", preset, maxsplit=2)[1] if not preset.startswith("local>") else org
if preset.startswith("npm:") or owner.lower() != org.lower():
yield "REVIEW", f"external preset {preset} (not owned by {org}; can set env and extends)"
if isinstance(d.get("env"), dict) and d["env"]:
risky = [k for k in d["env"] if k in ("PATH", "LD_PRELOAD", "NODE_OPTIONS", "GIT_SSL_CAINFO") or k.startswith(("LD_", "DYLD_"))]
yield ("HIGH" if risky else "INFO"), f"env sets {', '.join(d['env'])}"
for name, ds in (d.get("customDatasources") or {}).items():
url = ds.get("defaultRegistryUrlTemplate", "")
host = urlparse(url).hostname or ""
if host and internal(host):
yield "HIGH", f"customDatasource {name} points at internal address {url}"
org = sys.argv[sys.argv.index("--org") + 1] if "--org" in sys.argv else ""
repos = [a for i, a in enumerate(sys.argv[1:], 1) if a != "--org" and sys.argv[i - 1] != "--org"]
for repo in repos:
for name in CONFIG_NAMES + ("package.json",):
path = os.path.join(repo, name)
if os.path.isfile(path):
try:
findings = list(audit(load(path), org))
except (ValueError, KeyError) as e:
findings = [("ERROR", f"could not parse: {e}")]
for level, msg in findings:
print(f"{level:<7} {os.path.join(os.path.basename(repo), name)}: {msg}")
Run against three fixture repositories in a hypothetical organisation called acme:
$ python3 renovate_preset_audit.py --org acme fx/payments-api fx/web-app fx/infra
REVIEW payments-api/.github/renovate.json: external preset github>someone-else/shared-presets (not owned by acme; can set env and extends)
INFO payments-api/.github/renovate.json: env sets GOPRIVATE
REVIEW web-app/renovate.json5: HTTP preset https://presets.example.com/web.json
HIGH web-app/renovate.json5: customDatasource internal-tools points at internal address http://169.254.169.254/latest/meta-data/
HIGH infra/renovate.json: env sets NODE_OPTIONS
We also ran it against the Renovate configs of nine large open-source projects, including Grafana, Backstage, Cilium, Mastodon, Argo CD and Renovate itself. It found nothing to report. They extend only Renovate's built-in presets or presets from their own organisation, none sets env, and the one custom datasource points at a public URL. That fits the advisories: the risk is mostly in self-hosted setups where many teams share one Renovate instance and its credentials, and where repository owners can pull in whatever presets they like.
Hardening a self-hosted instance
- Upgrade. 44.79.0 at minimum; the latest release on 15 October was 44.161.2.
- Set
internalHostAccesstoblock. AddhostRulesfor internal registries and, withallowInternal, for any internal preset server, then check the warnings stop. - Keep
allowedEnvshort. Allow named variables that repositories genuinely need, never patterns that could matchPATH,LD_*orNODE_OPTIONS. - Don't turn on
exposeAllEnvunless you trust every repository Renovate runs against. Renovate's own documentation warns that it can expose secrets to a malicious script. - Give Renovate least-privilege cloud credentials. If it runs on a cloud VM or pod, make sure the role behind the metadata service can't do more than Renovate needs, and require IMDSv2 on AWS.
- Review external presets. Treat a preset from outside your organisation like a dependency: pin it to a tag or commit where the preset syntax allows, and know who maintains it.
The fair view
Both bugs needed someone who could change a repository's Renovate config, or control a preset that config already extends. In most companies that's any developer, which is why this matters for shared instances. Renovate's maintainers shipped both fixes in early September, before publishing the advisories, wrote clear impact conditions, and documented the new controls in detail. The weak point that remains is the default: until internalHostAccess defaults to block, the safe setting is a choice each administrator has to make.
The broader lesson applies to every bot that reads configuration from the repositories it works on. If repository config can choose URLs, environment variables or commands, the bot's credentials are only as safe as the least careful repository it serves. For more on running Renovate across an organisation, see our Renovate and Dependabot rollout guide.