From 1b6d7b8df5bde7ccfb5b56b454f85ac458969b32 Mon Sep 17 00:00:00 2001 From: artiphishle Date: Thu, 8 Oct 2026 09:07:19 +0200 Subject: [PATCH 1/2] test(apm): stabilize temporary path containment --- .../inspectPackedApmReleaseCandidateAsync.ts | 2 +- .../apm-release-validation/apmRelease.test.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/features/apm-release-validation/adapters/outbound/inspectPackedApmReleaseCandidateAsync.ts b/src/features/apm-release-validation/adapters/outbound/inspectPackedApmReleaseCandidateAsync.ts index 0b475e59..7a2ac5f8 100644 --- a/src/features/apm-release-validation/adapters/outbound/inspectPackedApmReleaseCandidateAsync.ts +++ b/src/features/apm-release-validation/adapters/outbound/inspectPackedApmReleaseCandidateAsync.ts @@ -28,8 +28,8 @@ export async function inspectPackedApmReleaseCandidateAsync( const temporaryRoot = await mkdtemp(resolve(tmpdir(), 'ankh-apm-release-')); try { const archive = resolve(temporaryRoot, 'candidate.tgz'); - const packageRoot = resolve(temporaryRoot, 'package'); await packCandidateAsync(root, temporaryRoot, archive); + const packageRoot = await realpath(resolve(temporaryRoot, 'package')); const candidate = await readPackedCandidateAsync(packageRoot, archive); const result = await validatePackedCandidateAsync(candidate, packageRoot, root, options); if (!result.valid || options.artifactPath === undefined) return result; diff --git a/src/features/apm-release-validation/apmRelease.test.ts b/src/features/apm-release-validation/apmRelease.test.ts index cc5e6415..b2a25b7e 100644 --- a/src/features/apm-release-validation/apmRelease.test.ts +++ b/src/features/apm-release-validation/apmRelease.test.ts @@ -76,6 +76,23 @@ test('packs outside the checkout, disables lifecycle scripts, and retains exact ); }); +test('packed validation accepts a canonicalized extracted root reached through a temporary symlink', async () => { + const root = await fixtureAsync(descriptor()); + const temporaryRoot = await temporaryRootAsync(); + const resolvedTemporaryRoot = join(temporaryRoot, 'resolved'); + const symlinkedTemporaryRoot = join(temporaryRoot, 'symlinked'); + await mkdir(resolvedTemporaryRoot); + await symlink(resolvedTemporaryRoot, symlinkedTemporaryRoot, 'dir'); + const originalTemporaryRoot = process.env.TMPDIR; + process.env.TMPDIR = symlinkedTemporaryRoot; + try { + expect(await validatePackedApmReleaseAsync(root)).toMatchObject({ valid: true, blockers: [] }); + } finally { + if (originalTemporaryRoot === undefined) delete process.env.TMPDIR; + else process.env.TMPDIR = originalTemporaryRoot; + } +}); + test('owner code requires consent and native conditional exports resolve from the packed package', async () => { const root = await fixtureAsync(projectionDescriptor()); expect((await validatePackedApmReleaseAsync(root)).blockers[0]).toContain('owner-code-consent'); From 7b6dff51b07a595dda9d63cb360aa56155028e02 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:57:04 +0200 Subject: [PATCH 2/2] fix(apm): add release intent for temp-root containment --- .changeset/calm-roads-fix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/calm-roads-fix.md diff --git a/.changeset/calm-roads-fix.md b/.changeset/calm-roads-fix.md new file mode 100644 index 00000000..20e0a3da --- /dev/null +++ b/.changeset/calm-roads-fix.md @@ -0,0 +1,5 @@ +--- +'@ankhorage/devtools': patch +--- + +Canonicalize packed APM extraction roots before containment validation so equivalent temporary realpaths are accepted without weakening traversal or symlink-escape protection.