feat(template): use environment variables in templates (#19301)

This commit is contained in:
Marek Grzenkowicz 2023-01-03 11:55:41 +01:00 committed by GitHub
parent 662fe78958
commit 2cd10769f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 55 additions and 3 deletions

View file

@ -363,8 +363,15 @@ If this option is not set, Renovate will fallback to 15 minutes.
## exposeAllEnv ## exposeAllEnv
To keep you safe, Renovate only passes a limited set of environment variables to package managers. 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. 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. Setting this to `true` also allows for variable substitution in `.npmrc` files.

View file

@ -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`. 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}}` `{{#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.

View file

@ -21,7 +21,7 @@ import type {
RawExecOptions, RawExecOptions,
} from './types'; } from './types';
function getChildEnv({ export function getChildEnv({
extraEnv, extraEnv,
env: forcedEnv = {}, env: forcedEnv = {},
}: ExecOptions): Record<string, string> { }: ExecOptions): Record<string, string> {

View file

@ -1,7 +1,20 @@
import { mocked } from '../../../test/util';
import { getOptions } from '../../config/options'; import { getOptions } from '../../config/options';
import * as _exec from '../exec';
import * as template from '.'; import * as template from '.';
jest.mock('../exec');
const exec = mocked(_exec);
describe('util/template/index', () => { describe('util/template/index', () => {
beforeEach(() => {
exec.getChildEnv.mockReturnValue({
CUSTOM_FOO: 'foo',
HOME: '/root',
});
});
it('has valid exposed config options', () => { it('has valid exposed config options', () => {
const allOptions = getOptions().map((option) => option.name); const allOptions = getOptions().map((option) => option.name);
const missingOptions = template.exposedConfigOptions.filter( const missingOptions = template.exposedConfigOptions.filter(
@ -85,6 +98,18 @@ describe('util/template/index', () => {
expect(output).toBe('foo'); 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', () => { describe('proxyCompileInput', () => {
const allowedField = 'body'; const allowedField = 'body';
const forbiddenField = 'foobar'; const forbiddenField = 'foobar';

View file

@ -2,6 +2,7 @@ import is from '@sindresorhus/is';
import handlebars from 'handlebars'; import handlebars from 'handlebars';
import { GlobalConfig } from '../../config/global'; import { GlobalConfig } from '../../config/global';
import { logger } from '../../logger'; import { logger } from '../../logger';
import { getChildEnv } from '../exec';
handlebars.registerHelper('encodeURIComponent', encodeURIComponent); handlebars.registerHelper('encodeURIComponent', encodeURIComponent);
handlebars.registerHelper('decodeURIComponent', decodeURIComponent); handlebars.registerHelper('decodeURIComponent', decodeURIComponent);
@ -170,6 +171,10 @@ const allowedTemplateFields = new Set([
const compileInputProxyHandler: ProxyHandler<CompileInput> = { const compileInputProxyHandler: ProxyHandler<CompileInput> = {
get(target: CompileInput, prop: keyof CompileInput): unknown { get(target: CompileInput, prop: keyof CompileInput): unknown {
if (prop === 'env') {
return target[prop];
}
if (!allowedTemplateFields.has(prop)) { if (!allowedTemplateFields.has(prop)) {
return undefined; return undefined;
} }
@ -202,13 +207,17 @@ export function compile(
input: CompileInput, input: CompileInput,
filterFields = true filterFields = true
): string { ): string {
const data = { ...GlobalConfig.get(), ...input }; const env = getChildEnv({});
const data = { ...GlobalConfig.get(), ...input, env };
const filteredInput = filterFields ? proxyCompileInput(data) : data; const filteredInput = filterFields ? proxyCompileInput(data) : data;
logger.trace({ template, filteredInput }, 'Compiling template'); logger.trace({ template, filteredInput }, 'Compiling template');
if (filterFields) { if (filterFields) {
const matches = template.matchAll(templateRegex); const matches = template.matchAll(templateRegex);
for (const match of matches) { for (const match of matches) {
const varNames = match[1].split('.'); const varNames = match[1].split('.');
if (varNames[0] === 'env') {
continue;
}
for (const varName of varNames) { for (const varName of varNames) {
if (!allowedFieldsList.includes(varName)) { if (!allowedFieldsList.includes(varName)) {
logger.info( logger.info(