From 6af436f5c972fd1a3693363258169da41f516f68 Mon Sep 17 00:00:00 2001 From: Jamie Feingold Date: Thu, 30 Jul 2026 12:22:49 -0500 Subject: [PATCH 1/2] fix: resolved zip-slip vulnerability (W-23558165) --- messages/sdr.md | 4 ++++ .../staticResourceMetadataTransformer.ts | 16 +++++++++---- .../staticResourceMetadataTransformer.test.ts | 24 +++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/messages/sdr.md b/messages/sdr.md index 1793c2b335..b1c9406e57 100644 --- a/messages/sdr.md +++ b/messages/sdr.md @@ -109,6 +109,10 @@ No source-backed components present in the package. No components in the package to retrieve. +# error_static_resource_attempting_zip_slip + +Entry '%s' in static resource '%s' resolves to a location outside the extraction directory ('%s'). + # error_static_resource_expected_archive_type A StaticResource directory must have a content type of application/zip or application/jar - found %s for %s. diff --git a/src/convert/transformers/staticResourceMetadataTransformer.ts b/src/convert/transformers/staticResourceMetadataTransformer.ts index 4fa05ac18f..f7e67d2619 100644 --- a/src/convert/transformers/staticResourceMetadataTransformer.ts +++ b/src/convert/transformers/staticResourceMetadataTransformer.ts @@ -132,10 +132,18 @@ export class StaticResourceMetadataTransformer extends BaseMetadataTransformer { for (const filePath of Object.keys(srZip.files)) { const zipObj = srZip.file(filePath); if (zipObj && !zipObj.dir) { - const path = join(baseContentPath, filePath); - const fullDest = isAbsolute(path) - ? path - : join(this.defaultDirectory ?? component.getPackageRelativePath('', 'source'), path); + const baseDestinationPath = isAbsolute(baseContentPath) + ? baseContentPath + : join(this.defaultDirectory ?? component.getPackageRelativePath('', 'source'), baseContentPath); + const fullDest = join(baseDestinationPath, filePath); + const relativeDest = relative(baseDestinationPath, fullDest); + if (relativeDest.startsWith('..') || isAbsolute(relativeDest)) { + throw messages.createError('error_static_resource_attempting_zip_slip', [ + filePath, + component.name, + baseDestinationPath, + ]); + } pipelinePromises.push(this.pipeline(new Readable().wrap(zipObj.nodeStream()), fullDest)); } } diff --git a/test/convert/transformers/staticResourceMetadataTransformer.test.ts b/test/convert/transformers/staticResourceMetadataTransformer.test.ts index eb367120b9..879c4faa4f 100644 --- a/test/convert/transformers/staticResourceMetadataTransformer.test.ts +++ b/test/convert/transformers/staticResourceMetadataTransformer.test.ts @@ -352,6 +352,30 @@ describe('StaticResourceMetadataTransformer', () => { expect(await transformer.toSourceFormat({ component })).to.deep.equalInAnyOrder(expectedInfos); }); + it('blocks static resources attempting zip-slip attack', async () => { + assert(typeof transformer.defaultDirectory === 'string'); + + const component = mixedContentSingleFile.COMPONENT; + const { xml } = component; + assert(xml); + env.stub(component, 'parseXml').resolves({ + StaticResource: { + contentType: 'application/zip', + }, + }); + + const filePath = join('..', '..', '..', 'b', 'c.css'); + const testZip = new JSZip().file(filePath, 'malicious css content'); + env.stub(JSZip, 'loadAsync').resolves(testZip); + + try { + void (await transformer.toSourceFormat({ component })); + assert.fail('SHOULD HAVE THROWN ERROR'); + } catch (error) { + expect((error as Error).message).to.include('resolves to a location outside the extraction directory'); + } + }); + it('should merge output with merge component when content is archive', async () => { const root = join('path', 'to', 'another', 'mixedSingleFiles'); const component = mixedContentSingleFile.COMPONENT; From 4f5d8cfbb976ae4b44e8fbe12e87524e54b4da4f Mon Sep 17 00:00:00 2001 From: Jamie Feingold Date: Fri, 31 Jul 2026 10:14:49 -0500 Subject: [PATCH 2/2] fix: feedback from code review (W-23558165) --- .../transformers/staticResourceMetadataTransformer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/convert/transformers/staticResourceMetadataTransformer.ts b/src/convert/transformers/staticResourceMetadataTransformer.ts index f7e67d2619..34bec390b7 100644 --- a/src/convert/transformers/staticResourceMetadataTransformer.ts +++ b/src/convert/transformers/staticResourceMetadataTransformer.ts @@ -129,12 +129,12 @@ export class StaticResourceMetadataTransformer extends BaseMetadataTransformer { const srZip = await getStaticResourceZip(component, content); const pipelinePromises: Array> = []; + const baseDestinationPath = isAbsolute(baseContentPath) + ? baseContentPath + : join(this.defaultDirectory ?? component.getPackageRelativePath('', 'source'), baseContentPath); for (const filePath of Object.keys(srZip.files)) { const zipObj = srZip.file(filePath); if (zipObj && !zipObj.dir) { - const baseDestinationPath = isAbsolute(baseContentPath) - ? baseContentPath - : join(this.defaultDirectory ?? component.getPackageRelativePath('', 'source'), baseContentPath); const fullDest = join(baseDestinationPath, filePath); const relativeDest = relative(baseDestinationPath, fullDest); if (relativeDest.startsWith('..') || isAbsolute(relativeDest)) {