Skip to content

Commit 7b1bf89

Browse files
committed
Replace archived lz4 package with actively maintained lz4-napi
The lz4 npm package (pierrec/node-lz4) is archived and bundles liblz4 v1.9.2, which is vulnerable to CVE-2021-3520 (CVSS 9.8). Replace it with lz4-napi, which uses Rust/napi-rs bindings and supports the LZ4 frame API required for result decompression. Resolves: SEC-15865, PECO-2020
1 parent 538556d commit 7b1bf89

7 files changed

Lines changed: 35 additions & 28 deletions

File tree

lib/result/ArrowResultHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default class ArrowResultHandler implements IResultsProvider<ArrowBatch>
2727
this.isLZ4Compressed = lz4Compressed ?? false;
2828

2929
if (this.isLZ4Compressed && !LZ4()) {
30-
throw new HiveDriverError('Cannot handle LZ4 compressed result: module `lz4` not installed');
30+
throw new HiveDriverError('Cannot handle LZ4 compressed result: module `lz4-napi` not installed');
3131
}
3232
}
3333

lib/result/CloudFetchResultHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default class CloudFetchResultHandler implements IResultsProvider<ArrowBa
2828
this.isLZ4Compressed = lz4Compressed ?? false;
2929

3030
if (this.isLZ4Compressed && !LZ4()) {
31-
throw new HiveDriverError('Cannot handle LZ4 compressed result: module `lz4` not installed');
31+
throw new HiveDriverError('Cannot handle LZ4 compressed result: module `lz4-napi` not installed');
3232
}
3333
}
3434

lib/utils/lz4.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
import type LZ4Namespace from 'lz4';
2-
3-
type LZ4Module = typeof LZ4Namespace;
1+
interface LZ4Module {
2+
encode(data: Buffer): Buffer;
3+
decode(data: Buffer): Buffer;
4+
}
45

56
function tryLoadLZ4Module(): LZ4Module | undefined {
67
try {
7-
return require('lz4'); // eslint-disable-line global-require
8+
// eslint-disable-next-line global-require
9+
const lz4napi = require('lz4-napi');
10+
return {
11+
encode: lz4napi.compressFrameSync,
12+
decode: lz4napi.decompressFrameSync,
13+
};
814
} catch (err) {
915
if (!(err instanceof Error) || !('code' in err)) {
1016
// eslint-disable-next-line no-console

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
"devDependencies": {
5050
"@types/chai": "^4.3.14",
5151
"@types/http-proxy": "^1.17.14",
52-
"@types/lz4": "^0.6.4",
5352
"@types/mocha": "^10.0.6",
5453
"@types/node": "^18.11.9",
5554
"@types/node-fetch": "^2.6.4",
@@ -89,6 +88,6 @@
8988
"winston": "^3.8.2"
9089
},
9190
"optionalDependencies": {
92-
"lz4": "^0.6.5"
91+
"lz4-napi": "^2.9.0"
9392
}
9493
}

tests/unit/result/ArrowResultHandler.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from 'chai';
22
import Int64 from 'node-int64';
3-
import LZ4 from 'lz4';
3+
import { compressFrameSync } from 'lz4-napi';
44
import ArrowResultHandler from '../../../lib/result/ArrowResultHandler';
55
import ResultsProviderStub from '../.stubs/ResultsProviderStub';
66
import { TRowSet, TSparkArrowBatch, TStatusCode, TTableSchema } from '../../../thrift/TCLIService_types';
@@ -39,7 +39,7 @@ const sampleRowSet1LZ4Compressed: TRowSet = {
3939
rows: [],
4040
arrowBatches: sampleRowSet1.arrowBatches?.map((item) => ({
4141
...item,
42-
batch: LZ4.encode(item.batch),
42+
batch: compressFrameSync(item.batch),
4343
})),
4444
};
4545

tests/unit/result/CloudFetchResultHandler.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { expect, AssertionError } from 'chai';
22
import sinon, { SinonStub } from 'sinon';
33
import Int64 from 'node-int64';
4-
import LZ4 from 'lz4';
4+
import { compressFrameSync } from 'lz4-napi';
55
import { Request, Response } from 'node-fetch';
66
import { ShouldRetryResult } from '../../../lib/connection/contracts/IRetryPolicy';
77
import { HttpTransactionDetails } from '../../../lib/connection/contracts/IConnectionProvider';
@@ -306,7 +306,7 @@ describe('CloudFetchResultHandler', () => {
306306

307307
context.invokeWithRetryStub.callsFake(async () => ({
308308
request: new Request('localhost'),
309-
response: new Response(LZ4.encode(expectedBatch), { status: 200 }),
309+
response: new Response(compressFrameSync(expectedBatch), { status: 200 }),
310310
}));
311311

312312
expect(await rowSetProvider.hasMore()).to.be.true;

tests/unit/utils/lz4.test.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ describe('lz4 module loader', () => {
2222
});
2323
});
2424

25-
const mockModuleLoad = (lz4MockOrError: unknown): { restore: () => void; wasLz4LoadAttempted: () => boolean } => {
25+
const mockModuleLoad = (
26+
lz4MockOrError: unknown,
27+
): { restore: () => void; wasLz4LoadAttempted: () => boolean } => {
2628
// eslint-disable-next-line global-require
2729
const Module = require('module');
2830
const originalLoad = Module._load;
2931
let lz4LoadAttempted = false;
3032

3133
Module._load = (request: string, parent: unknown, isMain: boolean) => {
32-
if (request === 'lz4') {
34+
if (request === 'lz4-napi') {
3335
lz4LoadAttempted = true;
3436
if (lz4MockOrError instanceof Error) {
3537
throw lz4MockOrError;
@@ -53,19 +55,19 @@ describe('lz4 module loader', () => {
5355
return require('../../../lib/utils/lz4');
5456
};
5557

56-
it('should successfully load and use lz4 module when available', () => {
57-
const fakeLz4 = {
58-
encode: (buf: Buffer) => {
58+
it('should successfully load and use lz4-napi module when available', () => {
59+
const fakeLz4Napi = {
60+
compressFrameSync: (buf: Buffer) => {
5961
const compressed = Buffer.from(`compressed:${buf.toString()}`);
6062
return compressed;
6163
},
62-
decode: (buf: Buffer) => {
64+
decompressFrameSync: (buf: Buffer) => {
6365
const decompressed = buf.toString().replace('compressed:', '');
6466
return Buffer.from(decompressed);
6567
},
6668
};
6769

68-
const { restore } = mockModuleLoad(fakeLz4);
70+
const { restore } = mockModuleLoad(fakeLz4Napi);
6971
const moduleExports = loadLz4Module();
7072
const lz4Module = moduleExports.default();
7173
restore();
@@ -82,8 +84,8 @@ describe('lz4 module loader', () => {
8284
expect(consoleWarnStub.called).to.be.false;
8385
});
8486

85-
it('should return undefined when lz4 module fails to load with MODULE_NOT_FOUND', () => {
86-
const err: NodeJS.ErrnoException = new Error("Cannot find module 'lz4'");
87+
it('should return undefined when lz4-napi module fails to load with MODULE_NOT_FOUND', () => {
88+
const err: NodeJS.ErrnoException = new Error("Cannot find module 'lz4-napi'");
8789
err.code = 'MODULE_NOT_FOUND';
8890

8991
const { restore } = mockModuleLoad(err);
@@ -95,7 +97,7 @@ describe('lz4 module loader', () => {
9597
expect(consoleWarnStub.called).to.be.false;
9698
});
9799

98-
it('should return undefined and log warning when lz4 fails with ERR_DLOPEN_FAILED', () => {
100+
it('should return undefined and log warning when lz4-napi fails with ERR_DLOPEN_FAILED', () => {
99101
const err: NodeJS.ErrnoException = new Error('Module did not self-register');
100102
err.code = 'ERR_DLOPEN_FAILED';
101103

@@ -109,7 +111,7 @@ describe('lz4 module loader', () => {
109111
expect(consoleWarnStub.firstCall.args[0]).to.include('Architecture or version mismatch');
110112
});
111113

112-
it('should return undefined and log warning when lz4 fails with unknown error code', () => {
114+
it('should return undefined and log warning when lz4-napi fails with unknown error code', () => {
113115
const err: NodeJS.ErrnoException = new Error('Some unknown error');
114116
err.code = 'UNKNOWN_ERROR';
115117

@@ -136,13 +138,13 @@ describe('lz4 module loader', () => {
136138
expect(consoleWarnStub.firstCall.args[0]).to.include('Invalid error object');
137139
});
138140

139-
it('should not attempt to load lz4 module when getResolvedModule is not called', () => {
140-
const fakeLz4 = {
141-
encode: () => Buffer.from(''),
142-
decode: () => Buffer.from(''),
141+
it('should not attempt to load lz4-napi module when getResolvedModule is not called', () => {
142+
const fakeLz4Napi = {
143+
compressFrameSync: () => Buffer.from(''),
144+
decompressFrameSync: () => Buffer.from(''),
143145
};
144146

145-
const { restore, wasLz4LoadAttempted } = mockModuleLoad(fakeLz4);
147+
const { restore, wasLz4LoadAttempted } = mockModuleLoad(fakeLz4Napi);
146148

147149
// Load the module but don't call getResolvedModule
148150
loadLz4Module();

0 commit comments

Comments
 (0)