diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index b0224431ab35f239e703ab56a98014fb704771bc..648cac44b693db6fe4e57d16bf6ff04a425ef7bf 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -363,8 +363,15 @@ If this option is not set, Renovate will fallback to 15 minutes. ## exposeAllEnv To keep you safe, Renovate only passes a limited set of environment variables to package managers. -Confidential data can be leaked if a malicious script enumerates all environment variables. +If you must expose all environment variables to package managers, you can set this option to `true`. + +<!-- prettier-ignore --> +!!! warning + Always consider the security implications of using `exposeAllEnv`! + Secrets and other confidential information stored in environment variables could be leaked by a malicious script, that enumerates all environment variables. + Set `exposeAllEnv` to `true` only if you have reviewed, and trust, the repositories which Renovate bot runs against. +Alternatively, you can use the [`customEnvVariables`](https://docs.renovatebot.com/self-hosted-configuration/#customenvvariables) config option to handpick a set of variables you need to expose. Setting this to `true` also allows for variable substitution in `.npmrc` files. diff --git a/docs/usage/templates.md b/docs/usage/templates.md index 8aa2a282be4e7d1e86e6483c5b9e986c25a90619..a7c10f79a06657ab0c970f7fd0120fb28e25cc3b 100644 --- a/docs/usage/templates.md +++ b/docs/usage/templates.md @@ -95,3 +95,14 @@ In the example above, it will only show a text if `isMajor=true` and `hasRelease Returns `true` if at least one expression is `true`. `{{#if (or isPatch isSingleVersion}}Small update, safer to merge and release.{{else}}Check out the changelog for all versions before merging!{{/if}}` + +## Environment variables + +By default, you can only access a handful of basic environment variables like `HOME` or `PATH`. +This is for security reasons. + +`HOME is {{env.HOME}}` + +If you're self-hosting Renovate, you can expose additional variables with the [`customEnvVariables`](https://docs.renovatebot.com/self-hosted-configuration/#customenvvariables) config option. + +You can also use the [`exposeAllEnv`](https://docs.renovatebot.com/self-hosted-configuration/#exposeallenv) config option to allow all environment variables in templates, but make sure to consider the security implications of giving the scripts unrestricted access to all variables. diff --git a/lib/util/exec/index.ts b/lib/util/exec/index.ts index 712e5fa3d06d5ceef31141b2712794d624be0530..ccb99ef13e9a423df1725ef0102751477c8ee170 100644 --- a/lib/util/exec/index.ts +++ b/lib/util/exec/index.ts @@ -21,7 +21,7 @@ import type { RawExecOptions, } from './types'; -function getChildEnv({ +export function getChildEnv({ extraEnv, env: forcedEnv = {}, }: ExecOptions): Record<string, string> { diff --git a/lib/util/template/index.spec.ts b/lib/util/template/index.spec.ts index a658988362211be0b911e36b1d07fc67d6e7d813..ec3e4fa6a1ea0a473d2cdf9012119d3240d2317a 100644 --- a/lib/util/template/index.spec.ts +++ b/lib/util/template/index.spec.ts @@ -1,7 +1,20 @@ +import { mocked } from '../../../test/util'; import { getOptions } from '../../config/options'; +import * as _exec from '../exec'; import * as template from '.'; +jest.mock('../exec'); + +const exec = mocked(_exec); + describe('util/template/index', () => { + beforeEach(() => { + exec.getChildEnv.mockReturnValue({ + CUSTOM_FOO: 'foo', + HOME: '/root', + }); + }); + it('has valid exposed config options', () => { const allOptions = getOptions().map((option) => option.name); const missingOptions = template.exposedConfigOptions.filter( @@ -85,6 +98,18 @@ describe('util/template/index', () => { expect(output).toBe('foo'); }); + it('has access to basic environment variables (basicEnvVars)', () => { + const userTemplate = 'HOME is {{env.HOME}}'; + const output = template.compile(userTemplate, {}); + expect(output).toBe('HOME is /root'); + }); + + it('and has access to custom variables (customEnvVariables)', () => { + const userTemplate = 'CUSTOM_FOO is {{env.CUSTOM_FOO}}'; + const output = template.compile(userTemplate, {}); + expect(output).toBe('CUSTOM_FOO is foo'); + }); + describe('proxyCompileInput', () => { const allowedField = 'body'; const forbiddenField = 'foobar'; diff --git a/lib/util/template/index.ts b/lib/util/template/index.ts index 4ff1ae2316e0da4440acad18491b7eaf1836f471..697c253671aaf2095ebc7623d0f1a9a76beac04e 100644 --- a/lib/util/template/index.ts +++ b/lib/util/template/index.ts @@ -2,6 +2,7 @@ import is from '@sindresorhus/is'; import handlebars from 'handlebars'; import { GlobalConfig } from '../../config/global'; import { logger } from '../../logger'; +import { getChildEnv } from '../exec'; handlebars.registerHelper('encodeURIComponent', encodeURIComponent); handlebars.registerHelper('decodeURIComponent', decodeURIComponent); @@ -170,6 +171,10 @@ const allowedTemplateFields = new Set([ const compileInputProxyHandler: ProxyHandler<CompileInput> = { get(target: CompileInput, prop: keyof CompileInput): unknown { + if (prop === 'env') { + return target[prop]; + } + if (!allowedTemplateFields.has(prop)) { return undefined; } @@ -202,13 +207,17 @@ export function compile( input: CompileInput, filterFields = true ): string { - const data = { ...GlobalConfig.get(), ...input }; + const env = getChildEnv({}); + const data = { ...GlobalConfig.get(), ...input, env }; const filteredInput = filterFields ? proxyCompileInput(data) : data; logger.trace({ template, filteredInput }, 'Compiling template'); if (filterFields) { const matches = template.matchAll(templateRegex); for (const match of matches) { const varNames = match[1].split('.'); + if (varNames[0] === 'env') { + continue; + } for (const varName of varNames) { if (!allowedFieldsList.includes(varName)) { logger.info(