Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions messages/sdr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 12 additions & 4 deletions src/convert/transformers/staticResourceMetadataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading