diff --git a/packages/targets/pkg-docker/src/index.test.ts b/packages/targets/pkg-docker/src/index.test.ts index 3ecda144..3654e5cb 100644 --- a/packages/targets/pkg-docker/src/index.test.ts +++ b/packages/targets/pkg-docker/src/index.test.ts @@ -112,4 +112,24 @@ describe('Docker buildx planning', () => { registries: [{ kind: 'custom' }], })).rejects.toThrow('pkg-docker requires a host for custom registry'); }); + + it('rejects unsafe Dockerfile and context paths before writing a plan', async () => { + await expect(adapter.build(fakeBuildContext() as any, { + image: 'acme/api', + registries: [{ kind: 'ghcr' }], + dockerfile: '../Dockerfile', + })).rejects.toThrow('pkg-docker dockerfile must not contain empty or parent path segments'); + + await expect(adapter.build(fakeBuildContext() as any, { + image: 'acme/api', + registries: [{ kind: 'ghcr' }], + context: '/tmp/project', + })).rejects.toThrow('pkg-docker context must be relative'); + + await expect(adapter.build(fakeBuildContext() as any, { + image: 'acme/api', + registries: [{ kind: 'ghcr' }], + context: 'apps\\api', + })).rejects.toThrow('pkg-docker context must use forward slashes'); + }); }); diff --git a/packages/targets/pkg-docker/src/index.ts b/packages/targets/pkg-docker/src/index.ts index 09aefb03..b2441b29 100644 --- a/packages/targets/pkg-docker/src/index.ts +++ b/packages/targets/pkg-docker/src/index.ts @@ -54,6 +54,22 @@ function tagValue(tag: string, version: string): string { .replaceAll('$version', normalized); } +function safeRelativePath(value: string | undefined, fallback: string, label: string): string { + const path = value ?? fallback; + if (path.length === 0) throw new Error(`pkg-docker ${label} must not be empty`); + if (path.startsWith('/')) throw new Error(`pkg-docker ${label} must be relative`); + if (path.includes('\\')) throw new Error(`pkg-docker ${label} must use forward slashes`); + if (path.includes('\0')) throw new Error(`pkg-docker ${label} contains invalid null bytes`); + const segments = path.split('/'); + if (segments.some((segment) => segment === '' || segment === '..')) { + throw new Error(`pkg-docker ${label} must not contain empty or parent path segments`); + } + if (label !== 'context' && segments.some((segment) => segment === '.')) { + throw new Error(`pkg-docker ${label} must not contain current-directory segments`); + } + return path; +} + function imageRefs(ctx: { version: string }, config: Config): string[] { const tags = [versionTag(ctx.version), 'latest', ...(config.tags ?? []).map((tag) => tagValue(tag, ctx.version))]; const uniqueTags = [...new Set(tags.filter(Boolean))]; @@ -69,8 +85,8 @@ function buildxArgs( opts: { push: boolean }, ): string[] { const platforms = config.platforms ?? ['linux/amd64', 'linux/arm64']; - const context = config.context ?? '.'; - const dockerfile = config.dockerfile ?? 'Dockerfile'; + const context = safeRelativePath(config.context, '.', 'context'); + const dockerfile = safeRelativePath(config.dockerfile, 'Dockerfile', 'dockerfile'); const args = [ 'buildx', 'build', @@ -115,8 +131,8 @@ export default defineTarget({ const plan = { image: config.image, version: versionTag(ctx.version), - context: config.context ?? '.', - dockerfile: config.dockerfile ?? 'Dockerfile', + context: safeRelativePath(config.context, '.', 'context'), + dockerfile: safeRelativePath(config.dockerfile, 'Dockerfile', 'dockerfile'), platforms, refs, commands: {