Cap conan package extraction size; require http(s) package URLs (#8820)

Fixes #8817. Kept deliberately simple per discussion — the conan server
is CE's own infrastructure, so these are hygiene bounds, not attack
mitigations:

- A generous fixed 2GiB cap on the total declared size of extracted
files (tar-stream enforces entry bodies match their headers, so summing
`header.size` bounds bytes written). Real packages are tens to a few
hundred MiB; hitting this means a packaging error or a corrupt/bombed
archive, and the extraction rejects cleanly through the existing error
path.
- The package URL conan returns must be http(s). No redirect
restrictions (conan may legitimately hand out redirecting/presigned
URLs), no config plumbing.

Both paths tested (cap exercised by lowering the limit on the instance
under test; scheme via a `file://` URL).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: mattgodbolt-molty <mattgodbolt-molty@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Matt Godbolt (bot acct)
2026-06-11 16:25:07 -05:00
committed by GitHub
parent 8e6033825a
commit 51317dc13f
2 changed files with 33 additions and 0 deletions

View File

@@ -65,6 +65,9 @@ export class BuildEnvSetupCeConanDirect extends BuildEnvSetupBase {
protected onlyonstaticliblink: any;
protected extractAllToRoot: boolean;
protected conan_os: string;
// Generous cap on the total declared size of extracted files; real library packages are
// tens to a few hundred MiB, so anything near this is a packaging error or a gzip bomb.
protected maxExtractedBytes = 2 * 1024 * 1024 * 1024;
static get key() {
return 'ceconan';
@@ -143,9 +146,15 @@ export class BuildEnvSetupCeConanDirect extends BuildEnvSetupBase {
downloadPath: string,
packageUrl: string,
): Promise<BuildEnvDownloadInfo> {
const protocol = new URL(packageUrl).protocol;
if (protocol !== 'http:' && protocol !== 'https:') {
throw new Error(`Unexpected protocol '${protocol}' for conan package URL`);
}
const startTime = process.hrtime.bigint();
const extract = tar.extract();
const gunzip = zlib.createGunzip();
let extractedBytes = 0;
extract.on('entry', async (header, stream, next) => {
// Drained streams (skipped entries) have no other error listener; without this an
@@ -167,6 +176,11 @@ export class BuildEnvSetupCeConanDirect extends BuildEnvSetupBase {
return;
}
extractedBytes += header.size ?? 0;
if (extractedBytes > this.maxExtractedBytes) {
throw new Error(`Package ${libId}/${version} exceeds ${this.maxExtractedBytes} bytes extracted`);
}
if (header.type !== 'file') {
// Only regular files are ever written; symlinks, hardlinks, directories etc
// are drained and skipped. Draining also avoids a hang on malformed entries

View File

@@ -192,6 +192,25 @@ describe('BuildEnvSetupCeConanDirect.downloadAndExtractPackage', () => {
await expect(fs.promises.access(path.join(downloadPath, 'somelib', 'evildir'))).rejects.toThrow();
}, 10_000);
it('rejects a package whose extracted size exceeds the cap', async () => {
const downloadPath = await temp.mkdir('ce-conan-test');
(setup as any).maxExtractedBytes = 1024;
try {
await expect(
setup.downloadAndExtractPackage('somelib', '1.0', downloadPath, `${baseUrl}/package.tgz`),
).rejects.toThrow(/exceeds 1024 bytes/);
} finally {
(setup as any).maxExtractedBytes = 2 * 1024 * 1024 * 1024;
}
});
it('rejects a non-http(s) package URL', async () => {
const downloadPath = await temp.mkdir('ce-conan-test');
await expect(
setup.downloadAndExtractPackage('somelib', '1.0', downloadPath, 'file:///etc/passwd'),
).rejects.toThrow(/Unexpected protocol 'file:'/);
});
it('rejects when the download is severed mid-stream', async () => {
const downloadPath = await temp.mkdir('ce-conan-test');
await expect(