Merge pull request #13054 from backstage/rugvip/safechild

backend-common: validate zip archive paths when unpacking into a dir
This commit is contained in:
Patrik Oldsberg
2022-08-09 19:27:59 +02:00
committed by GitHub
4 changed files with 30 additions and 2 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Added a second validation to the `dir()` method of ZIP archive responses returned from `readTree()` that ensures that extracted files do not fall outside the target directory.
@@ -28,6 +28,9 @@ const archiveDataCorrupted = fs.readFileSync(
const archiveDataWithExtraDir = fs.readFileSync(
resolvePath(__filename, '../../__fixtures__/mock-with-extra-root-dir.zip'),
);
const archiveWithMaliciousEntry = fs.readFileSync(
resolvePath(__filename, '../../__fixtures__/mallory.zip'),
);
describe('ZipArchiveResponse', () => {
beforeEach(() => {
@@ -35,6 +38,7 @@ describe('ZipArchiveResponse', () => {
'/test-archive.zip': archiveData,
'/test-archive-with-extra-root-dir.zip': archiveDataWithExtraDir,
'/test-archive-corrupted.zip': archiveDataCorrupted,
'/test-archive-malicious.zip': archiveWithMaliciousEntry,
'/tmp': mockFs.directory(),
});
});
@@ -167,4 +171,22 @@ describe('ZipArchiveResponse', () => {
'invalid comment length. expected: 55. found: 0',
);
});
it('should throw on entries with a path outside the destination dir', async () => {
const stream = fs.createReadStream('/test-archive-malicious.zip');
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag');
await expect(res.files()).rejects.toThrow(
'invalid relative path: ../side.txt',
);
});
it('should throw on entries that attempt to write outside destination dir', async () => {
const stream = fs.createReadStream('/test-archive-malicious.zip');
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag');
await expect(res.dir()).rejects.toThrow(
'invalid relative path: ../side.txt',
);
});
});
@@ -25,6 +25,7 @@ import {
ReadTreeResponseFile,
} from '../types';
import { streamToBuffer } from './util';
import { resolveSafeChildPath } from '../../paths';
/**
* Wraps a zip archive stream into a tree response reader.
@@ -187,10 +188,10 @@ export class ZipArchiveResponse implements ReadTreeResponse {
const dirname = platformPath.dirname(entryPath);
if (dirname) {
await fs.mkdirp(platformPath.join(dir, dirname));
await fs.mkdirp(resolveSafeChildPath(dir, dirname));
}
return new Promise(async (resolve, reject) => {
const file = fs.createWriteStream(platformPath.join(dir, entryPath));
const file = fs.createWriteStream(resolveSafeChildPath(dir, entryPath));
file.on('finish', resolve);
content.on('error', reject);