mirror of
https://github.com/docker/login-action.git
synced 2026-07-29 19:13:04 +00:00
harden buildx scoped config path handling
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
parent
371161bbe7
commit
8305724bcf
@ -35,6 +35,87 @@ test('getAuthList uses the default Docker Hub registry when computing scoped con
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('getAuthList supports @ scopes appended to the registry config dir', async () => {
|
||||||
|
process.env['INPUT_USERNAME'] = 'dbowie';
|
||||||
|
process.env['INPUT_PASSWORD'] = 'groundcontrol';
|
||||||
|
process.env['INPUT_SCOPE'] = '@push';
|
||||||
|
process.env['INPUT_LOGOUT'] = 'false';
|
||||||
|
const [auth] = getAuthList(getInputs());
|
||||||
|
expect(auth).toMatchObject({
|
||||||
|
configDir: path.join(Buildx.configDir, 'config', 'registry-1.docker.io') + '@push'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getAuthList supports repository scopes with appended actions', async () => {
|
||||||
|
process.env['INPUT_USERNAME'] = 'dbowie';
|
||||||
|
process.env['INPUT_PASSWORD'] = 'groundcontrol';
|
||||||
|
process.env['INPUT_SCOPE'] = 'docker/buildx-bin@push';
|
||||||
|
process.env['INPUT_LOGOUT'] = 'false';
|
||||||
|
const [auth] = getAuthList(getInputs());
|
||||||
|
expect(auth).toMatchObject({
|
||||||
|
configDir: path.join(Buildx.configDir, 'config', 'registry-1.docker.io', 'docker', 'buildx-bin@push')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getAuthList supports comma-separated scope actions', async () => {
|
||||||
|
process.env['INPUT_USERNAME'] = 'dbowie';
|
||||||
|
process.env['INPUT_PASSWORD'] = 'groundcontrol';
|
||||||
|
process.env['INPUT_SCOPE'] = 'docker/buildx-bin@pull,push';
|
||||||
|
process.env['INPUT_LOGOUT'] = 'false';
|
||||||
|
const [auth] = getAuthList(getInputs());
|
||||||
|
expect(auth).toMatchObject({
|
||||||
|
configDir: path.join(Buildx.configDir, 'config', 'registry-1.docker.io', 'docker', 'buildx-bin@pull,push')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
test.each([
|
||||||
|
'../../../../work/leaked',
|
||||||
|
'..',
|
||||||
|
'foo/../../../../etc',
|
||||||
|
'@../../../leaked',
|
||||||
|
'foo/bar@../../../leaked',
|
||||||
|
'@push@pull',
|
||||||
|
'foo/bar@push@pull',
|
||||||
|
'@push,',
|
||||||
|
'@,push',
|
||||||
|
'@pull,,push',
|
||||||
|
'@Push',
|
||||||
|
path.join(path.parse(Buildx.configDir).root, 'work', 'leaked')
|
||||||
|
])('getAuthList rejects unsafe or unsupported scope path: %s', async scope => {
|
||||||
|
expect(() => {
|
||||||
|
getAuthList({
|
||||||
|
registry: '',
|
||||||
|
username: 'dbowie',
|
||||||
|
password: 'groundcontrol',
|
||||||
|
scope,
|
||||||
|
ecr: '',
|
||||||
|
logout: false,
|
||||||
|
registryAuth: ''
|
||||||
|
});
|
||||||
|
}).toThrow(/Invalid scope/);
|
||||||
|
});
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
test.each([
|
||||||
|
'../../../../work/leaked',
|
||||||
|
'..',
|
||||||
|
'foo/../../../../etc',
|
||||||
|
path.join(path.parse(Buildx.configDir).root, 'work', 'leaked')
|
||||||
|
])('getAuthList rejects unsafe registry path: %s', async registry => {
|
||||||
|
expect(() => {
|
||||||
|
getAuthList({
|
||||||
|
registry,
|
||||||
|
username: 'dbowie',
|
||||||
|
password: 'groundcontrol',
|
||||||
|
scope: '@push',
|
||||||
|
ecr: '',
|
||||||
|
logout: false,
|
||||||
|
registryAuth: ''
|
||||||
|
});
|
||||||
|
}).toThrow(/Invalid registry/);
|
||||||
|
});
|
||||||
|
|
||||||
test('getAuthList skips secret masking when registry-auth password is absent', async () => {
|
test('getAuthList skips secret masking when registry-auth password is absent', async () => {
|
||||||
const stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
const stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
||||||
const [auth] = getAuthList({
|
const [auth] = getAuthList({
|
||||||
|
|||||||
@ -77,13 +77,33 @@ export function scopeToConfigDir(registry: string, scope?: string): string {
|
|||||||
if (scopeDisabled() || !scope || scope === '') {
|
if (scopeDisabled() || !scope || scope === '') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
let configDir = path.join(Buildx.configDir, 'config', registry === 'docker.io' ? 'registry-1.docker.io' : registry);
|
const configRoot = path.resolve(Buildx.configDir, 'config');
|
||||||
if (scope.startsWith('@')) {
|
const registryDir = path.resolve(configRoot, registry === 'docker.io' ? 'registry-1.docker.io' : registry);
|
||||||
configDir += scope;
|
if (!isChildPath(configRoot, registryDir)) {
|
||||||
} else {
|
throw new Error(`Invalid registry '${registry}': resolved config path escapes the Buildx config directory`);
|
||||||
configDir = path.join(configDir, scope);
|
|
||||||
}
|
}
|
||||||
return configDir;
|
const scopeParts = scope.split('@');
|
||||||
|
if (scopeParts.length > 2) {
|
||||||
|
throw new Error(`Invalid scope '${scope}': scope can contain at most one @ separator`);
|
||||||
|
}
|
||||||
|
const [scopePath, scopeActions] = scopeParts;
|
||||||
|
if (scopeActions !== undefined && !/^[a-z]+(,[a-z]+)*$/.test(scopeActions)) {
|
||||||
|
throw new Error(`Invalid scope '${scope}': scope actions must be lowercase names separated by commas`);
|
||||||
|
}
|
||||||
|
const scopeSuffix = scopeActions === undefined ? '' : `@${scopeActions}`;
|
||||||
|
if (scopePath === '') {
|
||||||
|
return `${registryDir}${scopeSuffix}`;
|
||||||
|
}
|
||||||
|
const configDir = path.resolve(registryDir, scopePath);
|
||||||
|
if (!isChildPath(registryDir, configDir)) {
|
||||||
|
throw new Error(`Invalid scope '${scope}': resolved config path escapes the Buildx config directory`);
|
||||||
|
}
|
||||||
|
return `${configDir}${scopeSuffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isChildPath(parent: string, child: string): boolean {
|
||||||
|
const relativePath = path.relative(parent, child);
|
||||||
|
return relativePath !== '' && relativePath !== '..' && !relativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scopeDisabled(): boolean {
|
function scopeDisabled(): boolean {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user