Skip to content
This repository was archived by the owner on Feb 20, 2026. It is now read-only.

Commit ef0f58c

Browse files
authored
security: replace vulnerable parse-duration with CJS compatible ms library (#960)
* security: replace vulnerable parse-duration with CJS compatible ms library * add comment about duration format
1 parent c55a8c4 commit ef0f58c

4 files changed

Lines changed: 18 additions & 85 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"delay": "^5.0.0",
3737
"extend": "^3.0.2",
3838
"gcp-metadata": "^6.0.0",
39-
"parse-duration": "^1.0.0",
39+
"ms": "^2.1.3",
4040
"pprof": "4.0.0",
4141
"pretty-ms": "^7.0.0",
4242
"protobufjs": "~7.4.0",
@@ -47,6 +47,7 @@
4747
"@types/extend": "^3.0.0",
4848
"@types/long": "^5.0.0",
4949
"@types/mocha": "^9.0.0",
50+
"@types/ms": "^2.1.0",
5051
"@types/nock": "^11.0.0",
5152
"@types/node": "^20.4.9",
5253
"@types/pretty-ms": "^5.0.0",

src/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// limitations under the License.
1414

1515
import {GoogleAuthOptions} from '@google-cloud/common';
16-
import parseDuration from 'parse-duration';
16+
import * as ms from 'ms';
1717

1818
// Configuration for Profiler.
1919
export interface Config extends GoogleAuthOptions {
@@ -185,7 +185,7 @@ export const defaultConfig = {
185185
heapMaxStackDepth: 64,
186186
ignoreHeapSamplesPath: '@google-cloud/profiler',
187187
initialBackoffMillis: 60 * 1000, // 1 minute
188-
backoffCapMillis: parseDuration('1h')!,
188+
backoffCapMillis: ms('1h')!,
189189
backoffMultiplier: 1.3,
190190
apiEndpoint: 'cloudprofiler.googleapis.com',
191191

src/profiler.ts

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import * as r from 'teeny-request';
2929
import {ProfilerConfig} from './config';
3030
import {createLogger} from './logger';
3131

32-
import parseDuration from 'parse-duration';
32+
import * as ms from 'ms';
3333
// eslint-disable-next-line @typescript-eslint/no-var-requires
3434
const pjson = require('../../package.json');
3535
const SCOPE = 'https://www.googleapis.com/auth/monitoring.write';
@@ -111,7 +111,12 @@ function getServerResponseBackoff(body: object): number | undefined {
111111
item.retryDelay &&
112112
typeof item.retryDelay === 'string'
113113
) {
114-
const backoffMillis = parseDuration(item.retryDelay)!;
114+
// item is a RetryInfo
115+
// https://github.com/googleapis/googleapis/blob/4ec607bd375cddbec6d28bc1931eab7da221e4bb/google/rpc/error_details.proto#L92
116+
// and the ProtoJSON encoding of the duration will be a string of seconds with "s"
117+
// suffix https://protobuf.dev/programming-guides/json
118+
const retryDelay: `${number}s` = item.retryDelay;
119+
const backoffMillis = ms(retryDelay);
115120
if (backoffMillis > 0) {
116121
return backoffMillis;
117122
}
@@ -121,30 +126,6 @@ function getServerResponseBackoff(body: object): number | undefined {
121126
return undefined;
122127
}
123128

124-
/**
125-
* @return if the backoff duration can be parsed, then the backoff duration in
126-
* ms, otherwise undefined.
127-
*
128-
* Public for testing.
129-
*/
130-
export function parseBackoffDuration(
131-
backoffMessage: string
132-
): number | undefined {
133-
const backoffMessageRegex =
134-
/action throttled, backoff for ((?:([0-9]+)h)?(?:([0-9]+)m)?([0-9.]+)s)$/;
135-
const [, duration] = backoffMessageRegex.exec(backoffMessage) || [
136-
undefined,
137-
undefined,
138-
];
139-
if (duration) {
140-
const backoffMillis = parseDuration(duration)!;
141-
if (backoffMillis > 0) {
142-
return backoffMillis;
143-
}
144-
}
145-
return undefined;
146-
}
147-
148129
/**
149130
* @return true if an deployment is a Deployment and false otherwise.
150131
*/
@@ -452,7 +433,7 @@ export class Profiler extends ServiceObject {
452433
// Default timeout for for a request is 1 minute, but request to create
453434
// profile is designed to hang until it is time to collect a profile
454435
// (up to one hour).
455-
timeout: parseDuration('1h')!,
436+
timeout: ms('1h')!,
456437
};
457438

458439
this.logger.debug('Attempting to create profile.');
@@ -551,7 +532,7 @@ export class Profiler extends ServiceObject {
551532
if (prof.duration === undefined) {
552533
throw Error('Cannot collect time profile, duration is undefined.');
553534
}
554-
const durationMillis = parseDuration(prof.duration);
535+
const durationMillis = ms(prof.duration as ms.StringValue);
555536
if (!durationMillis) {
556537
throw Error(
557538
`Cannot collect time profile, duration "${prof.duration}" cannot` +

test/test-profiler.ts

Lines changed: 5 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,7 @@ import * as zlib from 'zlib';
2828

2929
import {perftools} from 'pprof/proto/profile';
3030
import {ProfilerConfig} from '../src/config';
31-
import {
32-
parseBackoffDuration,
33-
Profiler,
34-
Retryer,
35-
BackoffResponseError,
36-
} from '../src/profiler';
31+
import {Profiler, Retryer, BackoffResponseError} from '../src/profiler';
3732

3833
import {
3934
decodedHeapProfile,
@@ -42,7 +37,7 @@ import {
4237
timeProfile,
4338
} from './profiles-for-tests';
4439

45-
import parseDuration from 'parse-duration';
40+
import * as ms from 'ms';
4641
// eslint-disable-next-line @typescript-eslint/no-var-requires
4742
const fakeCredentials = require('../../test/fixtures/gcloud-credentials.json');
4843

@@ -66,9 +61,9 @@ const testConfig: ProfilerConfig = {
6661
heapMaxStackDepth: 64,
6762
ignoreHeapSamplesPath: '@google-cloud/profiler',
6863
initialBackoffMillis: 1000,
69-
backoffCapMillis: parseDuration('1h')!,
64+
backoffCapMillis: ms('1h')!,
7065
backoffMultiplier: 1.3,
71-
serverBackoffCapMillis: parseDuration('7d')!,
66+
serverBackoffCapMillis: ms('7d')!,
7267
localProfilingPeriodMillis: 1000,
7368
localTimeDurationMillis: 1000,
7469
localLogPeriodMillis: 1000,
@@ -891,7 +886,7 @@ describe('Profiler', () => {
891886
);
892887
const profiler = new Profiler(testConfig);
893888
const delayMillis = await profiler.collectProfile();
894-
assert.strictEqual(parseDuration('7d'), delayMillis);
889+
assert.strictEqual(ms('7d'), delayMillis);
895890
}
896891
);
897892
it(
@@ -919,48 +914,4 @@ describe('Profiler', () => {
919914
}
920915
);
921916
});
922-
describe('parseBackoffDuration', () => {
923-
it('should return undefined when no duration specified', () => {
924-
assert.strictEqual(undefined, parseBackoffDuration(''));
925-
});
926-
it('should parse backoff with minutes and seconds specified', () => {
927-
assert.strictEqual(
928-
62000,
929-
parseBackoffDuration('action throttled, backoff for 1m2s')
930-
);
931-
});
932-
it('should parse backoff with fraction of second', () => {
933-
assert.strictEqual(
934-
2500,
935-
parseBackoffDuration('action throttled, backoff for 2.5s')
936-
);
937-
});
938-
it('should parse backoff with minutes and seconds, including fraction of second', () => {
939-
assert.strictEqual(
940-
62500,
941-
parseBackoffDuration('action throttled, backoff for 1m2.5s')
942-
);
943-
});
944-
it('should parse backoff with hours and seconds', () => {
945-
assert.strictEqual(
946-
3602500,
947-
parseBackoffDuration('action throttled, backoff for 1h2.5s')
948-
);
949-
});
950-
it('should parse backoff with hours, minutes, and seconds', () => {
951-
assert.strictEqual(
952-
3662500,
953-
parseBackoffDuration('action throttled, backoff for 1h1m2.5s')
954-
);
955-
});
956-
it('should parse return undefined for unexpected backoff time string format', () => {
957-
assert.strictEqual(
958-
undefined,
959-
parseBackoffDuration('action throttled, backoff for 1m2+s')
960-
);
961-
});
962-
it('should parse return undefined for unexpected string format', () => {
963-
assert.strictEqual(undefined, parseBackoffDuration('time 1m2s'));
964-
});
965-
});
966917
});

0 commit comments

Comments
 (0)