From bba0f24eaa0fffe3bb2fb71b5762e54c9c9a3546 Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Tue, 5 Dec 2023 22:28:49 +0000 Subject: [PATCH] fix(cactus-common): coerceUnknownToError() now uses HTML sanitize 1. This is a security fix so that the exception serialization does not accidentally XSS anybody who is looking at crash logs through some admin GUI that is designed to show logs that are considered trusted. Related discussion about `1)` can be seen at this other pull request: https://github.com/hyperledger/cacti/pull/2893 Signed-off-by: Peter Somogyvari --- .../exception/coerce-unknown-to-error.ts | 6 +- packages/cactus-core/package.json | 2 + .../handle-rest-endpoint-exception.ts | 32 ++- .../handle-rest-endpoint-exception.test.ts | 211 ++++++++++++++++++ .../delete-keychain-entry-endpoint-v1.ts | 3 +- .../get-keychain-entry-endpoint-v1.ts | 3 +- .../has-keychain-entry-endpoint-v1.ts | 3 +- .../set-keychain-entry-endpoint-v1.ts | 3 +- ...loy-contract-solidity-bytecode-endpoint.ts | 3 +- yarn.lock | 50 ++++- 10 files changed, 289 insertions(+), 27 deletions(-) create mode 100644 packages/cactus-core/src/test/typescript/unit/handle-rest-endpoint-exception.test.ts diff --git a/packages/cactus-common/src/main/typescript/exception/coerce-unknown-to-error.ts b/packages/cactus-common/src/main/typescript/exception/coerce-unknown-to-error.ts index cc1e6f2419..bdb9232252 100644 --- a/packages/cactus-common/src/main/typescript/exception/coerce-unknown-to-error.ts +++ b/packages/cactus-common/src/main/typescript/exception/coerce-unknown-to-error.ts @@ -1,4 +1,5 @@ import stringify from "fast-safe-stringify"; +import sanitizeHtml from "sanitize-html"; import { ErrorFromUnknownThrowable } from "./error-from-unknown-throwable"; import { ErrorFromSymbol } from "./error-from-symbol"; @@ -26,10 +27,11 @@ export function coerceUnknownToError(x: unknown): Error { } else if (x instanceof Error) { return x; } else { - const xAsJson = stringify(x, (_, value) => + const xAsJsonUnsafe = stringify(x, (_, value) => typeof value === "bigint" ? value.toString() + "n" : value, ); - return new ErrorFromUnknownThrowable(xAsJson); + const xAsJsonSanitized = sanitizeHtml(xAsJsonUnsafe); + return new ErrorFromUnknownThrowable(xAsJsonSanitized); } } diff --git a/packages/cactus-core/package.json b/packages/cactus-core/package.json index d1f0dd44e8..1913d39c06 100644 --- a/packages/cactus-core/package.json +++ b/packages/cactus-core/package.json @@ -56,6 +56,7 @@ "express-jwt-authz": "2.4.1", "express-openapi-validator": "5.0.4", "http-errors": "2.0.0", + "http-errors-enhanced": "1.1.2", "run-time-error-cjs": "1.4.0", "safe-stable-stringify": "2.4.3", "typescript-optional": "2.0.1" @@ -63,6 +64,7 @@ "devDependencies": { "@types/express": "4.17.19", "@types/http-errors": "2.0.2", + "node-mocks-http": "1.14.0", "uuid": "8.3.2" }, "engines": { diff --git a/packages/cactus-core/src/main/typescript/web-services/handle-rest-endpoint-exception.ts b/packages/cactus-core/src/main/typescript/web-services/handle-rest-endpoint-exception.ts index 66d291df8d..56b02462bc 100644 --- a/packages/cactus-core/src/main/typescript/web-services/handle-rest-endpoint-exception.ts +++ b/packages/cactus-core/src/main/typescript/web-services/handle-rest-endpoint-exception.ts @@ -1,12 +1,13 @@ import { Logger, createRuntimeErrorWithCause, + safeStringifyException, } from "@hyperledger/cactus-common"; import type { Response } from "express"; import createHttpError from "http-errors"; /** - * An interface describing the object contaiing the contextual information needed by the + * An interface describing the object containing the contextual information needed by the * `#handleRestEndpointException()` method to perform its duties. * * @param ctx - An object containing options for handling the REST endpoint exception. @@ -35,39 +36,46 @@ export interface IHandleRestEndpointExceptionOptions { * * @param ctx - An object containing options for handling the REST endpoint exception. */ -export function handleRestEndpointException( +export async function handleRestEndpointException( ctx: Readonly, -): void { +): Promise { + const errorAsSanitizedJson = safeStringifyException(ctx.error); + + const { identifierByCodes, INTERNAL_SERVER_ERROR } = await import( + "http-errors-enhanced" + ); + if (createHttpError.isHttpError(ctx.error)) { ctx.res.status(ctx.error.statusCode); // Log either an error or a debug message depending on what the statusCode is // For 5xx errors we treat it as a production bug that needs to be fixed on // our side and for everything else we treat it a user error and debug log it. - if (ctx.error.statusCode >= 500) { - ctx.log.debug(ctx.errorMsg, ctx.error); + if (ctx.error.statusCode >= INTERNAL_SERVER_ERROR) { + ctx.log.debug(ctx.errorMsg, errorAsSanitizedJson); } else { - ctx.log.error(ctx.errorMsg, ctx.error); + ctx.log.error(ctx.errorMsg, errorAsSanitizedJson); } // If the `expose` property is set to true it implies that we can safely // expose the contents of the exception to the calling client. if (ctx.error.expose) { ctx.res.json({ - message: ctx.error.message, - error: ctx.error, + message: identifierByCodes[ctx.error.statusCode], + error: errorAsSanitizedJson, }); } } else { // If the exception was not an http-error then we assume it was an internal // error (e.g. same behavior as if we had received an HTTP 500 statusCode) - ctx.log.error(ctx.errorMsg, ctx.error); + ctx.log.error(ctx.errorMsg, errorAsSanitizedJson); const rex = createRuntimeErrorWithCause(ctx.errorMsg, ctx.error); + const sanitizedJsonRex = safeStringifyException(rex); - ctx.res.status(500).json({ - message: "Internal Server Error", - error: rex, + ctx.res.status(INTERNAL_SERVER_ERROR).json({ + message: identifierByCodes[INTERNAL_SERVER_ERROR], + error: sanitizedJsonRex, }); } } diff --git a/packages/cactus-core/src/test/typescript/unit/handle-rest-endpoint-exception.test.ts b/packages/cactus-core/src/test/typescript/unit/handle-rest-endpoint-exception.test.ts new file mode 100644 index 0000000000..35fd6218d5 --- /dev/null +++ b/packages/cactus-core/src/test/typescript/unit/handle-rest-endpoint-exception.test.ts @@ -0,0 +1,211 @@ +import "jest-extended"; +import createHttpError from "http-errors"; +import { createResponse } from "node-mocks-http"; + +import { safeStringifyException } from "@hyperledger/cactus-common"; + +import { + handleRestEndpointException, + IHandleRestEndpointExceptionOptions, +} from "../../../main/typescript/public-api"; // replace with the correct path to your module + +import { LoggerProvider } from "@hyperledger/cactus-common"; + +const log = LoggerProvider.getOrCreate({ + label: "handle-rest-endpoint-exception.test.ts", + level: "DEBUG", +}); + +describe("handleRestEndpointException", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should handle HttpError with statusCode >= 500", async () => { + const mockResponse = createResponse(); + + const mockOptions: IHandleRestEndpointExceptionOptions = { + errorMsg: "Test error message", + log: jest.mocked(log), // Provide a mock logger if needed + error: new Error("Test error"), // Provide appropriate error objects for testing + res: mockResponse, + }; + + const mockHttpError = createHttpError(500, "Test HTTP error", { + expose: true, + }); + + const errorAsSanitizedJson = safeStringifyException(mockHttpError); + const spyLogDebug = jest.spyOn(mockOptions.log, "debug"); + const spyStatus = jest.spyOn(mockResponse, "status"); + const spyJson = jest.spyOn(mockResponse, "json"); + + await handleRestEndpointException({ ...mockOptions, error: mockHttpError }); + + expect(spyStatus).toHaveBeenCalledWith(500); + + expect(spyLogDebug).toHaveBeenCalledWith( + mockOptions.errorMsg, + errorAsSanitizedJson, + ); + + expect(spyJson).toHaveBeenCalledWith({ + message: "InternalServerError", + error: errorAsSanitizedJson, + }); + }); + + it("should handle HttpError with statusCode < 500", async () => { + const mockResponse = createResponse(); + + const mockOptions: IHandleRestEndpointExceptionOptions = { + errorMsg: "Test error message", + log: jest.mocked(log), // Provide a mock logger if needed + error: new Error("Test error"), // Provide appropriate error objects for testing + res: mockResponse, + }; + + const mockHttpError = createHttpError(404, "Test HTTP error", { + expose: true, + }); + + const errorAsSanitizedJson = safeStringifyException(mockHttpError); + const spyLogError = jest.spyOn(mockOptions.log, "error"); + const spyStatus = jest.spyOn(mockResponse, "status"); + const spyJson = jest.spyOn(mockResponse, "json"); + await handleRestEndpointException({ ...mockOptions, error: mockHttpError }); + + expect(spyStatus).toHaveBeenCalledWith(404); + expect(spyLogError).toHaveBeenCalledWith( + mockOptions.errorMsg, + errorAsSanitizedJson, + ); + + expect(spyJson).toHaveBeenCalledWith({ + message: "NotFound", + error: errorAsSanitizedJson, + }); + }); + + it("should handle non-HttpError", async () => { + const mockResponse = createResponse(); + + const mockError = new Error("An unexpected exception. Ha!"); + + const mockOptions: IHandleRestEndpointExceptionOptions = { + errorMsg: "Test error message", + log: jest.mocked(log), // Provide a mock logger if needed + error: mockError, + res: mockResponse, + }; + + const mockErrorJson = safeStringifyException(mockError); + const spyLoggerFn = jest.spyOn(mockOptions.log, "error"); + const spyStatus = jest.spyOn(mockResponse, "status"); + const spyJson = jest.spyOn(mockResponse, "json"); + + await handleRestEndpointException({ ...mockOptions, error: mockError }); + + expect(spyStatus).toHaveBeenCalledWith(500); + + expect(spyLoggerFn).toHaveBeenCalledWith( + mockOptions.errorMsg, + mockErrorJson, + ); + + expect(spyJson).toHaveBeenCalledOnce(); + + const mostRecentCall = spyJson.mock.lastCall; + expect(mostRecentCall).toBeTruthy(); + + expect(mostRecentCall?.[0].message).toBeString(); + expect(mostRecentCall?.[0].message).toEqual("InternalServerError"); + expect(mostRecentCall?.[0].error).toMatch( + /RuntimeError: Test error message(.*)An unexpected exception. Ha!/, + ); + }); + + it("should escape malicious payloads in exception messages", async () => { + const mockResponse = createResponse(); + + const dummyXssPayload = ``; + const mockError = new Error(dummyXssPayload); + + const mockOptions: IHandleRestEndpointExceptionOptions = { + errorMsg: "Test error message", + log: jest.mocked(log), // Provide a mock logger if needed + error: mockError, + res: mockResponse, + }; + + const mockErrorJson = safeStringifyException(mockError); + const spyLoggerFn = jest.spyOn(mockOptions.log, "error"); + const spyStatus = jest.spyOn(mockResponse, "status"); + const spyJson = jest.spyOn(mockResponse, "json"); + + await handleRestEndpointException({ ...mockOptions, error: mockError }); + + expect(spyStatus).toHaveBeenCalledWith(500); + + expect(spyLoggerFn).toHaveBeenCalledWith( + mockOptions.errorMsg, + mockErrorJson, + ); + + expect(spyJson).toHaveBeenCalledOnce(); + + const mostRecentCall = spyJson.mock.lastCall; + expect(mostRecentCall).toBeTruthy(); + + expect(mostRecentCall?.[0].message).toBeString(); + expect(mostRecentCall?.[0].message).toEqual("InternalServerError"); + expect(mostRecentCall?.[0].error).toMatch( + /RuntimeError: Test error message(.*)/, + ); + expect(mostRecentCall?.[0].error).not.toMatch( + /.*stealAndUploadPrivateKeys.*/, + ); + }); + + it("should escape malicious payloads in strings thrown", async () => { + const mockResponse = createResponse(); + + const dummyXssPayload = ``; + const mockError = dummyXssPayload; + + const mockOptions: IHandleRestEndpointExceptionOptions = { + errorMsg: "Test error message", + log: jest.mocked(log), // Provide a mock logger if needed + error: mockError, + res: mockResponse, + }; + + const mockErrorJson = safeStringifyException(mockError); + const spyLoggerFn = jest.spyOn(mockOptions.log, "error"); + const spyStatus = jest.spyOn(mockResponse, "status"); + const spyJson = jest.spyOn(mockResponse, "json"); + + await handleRestEndpointException({ ...mockOptions, error: mockError }); + + expect(spyStatus).toHaveBeenCalledWith(500); + + expect(spyLoggerFn).toHaveBeenCalledWith( + mockOptions.errorMsg, + mockErrorJson, + ); + + expect(spyJson).toHaveBeenCalledOnce(); + + const mostRecentCall = spyJson.mock.lastCall; + expect(mostRecentCall).toBeTruthy(); + + expect(mostRecentCall?.[0].message).toBeString(); + expect(mostRecentCall?.[0].message).toEqual("InternalServerError"); + expect(mostRecentCall?.[0].error).toMatch( + /RuntimeError: Test error message(.*)/, + ); + expect(mostRecentCall?.[0].error).not.toMatch( + /.*stealAndUploadPrivateKeys.*/, + ); + }); +}); diff --git a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/delete-keychain-entry-endpoint-v1.ts b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/delete-keychain-entry-endpoint-v1.ts index f247b1b294..33f9ab3874 100644 --- a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/delete-keychain-entry-endpoint-v1.ts +++ b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/delete-keychain-entry-endpoint-v1.ts @@ -87,6 +87,7 @@ export class DeleteKeychainEntryV1Endpoint implements IWebServiceEndpoint { } public async handleRequest(req: Request, res: Response): Promise { + const { log } = this; const fnTag = `${this.className}#handleRequest()`; const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); @@ -96,7 +97,7 @@ export class DeleteKeychainEntryV1Endpoint implements IWebServiceEndpoint { res.json(resBody); } catch (ex) { const errorMsg = `${reqTag} ${fnTag} Failed to deploy contract:`; - handleRestEndpointException({ errorMsg, log: this.log, error: ex, res }); + await handleRestEndpointException({ errorMsg, log, error: ex, res }); } } } diff --git a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/get-keychain-entry-endpoint-v1.ts b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/get-keychain-entry-endpoint-v1.ts index ee8bb4fce4..87ed6b2ce8 100644 --- a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/get-keychain-entry-endpoint-v1.ts +++ b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/get-keychain-entry-endpoint-v1.ts @@ -87,6 +87,7 @@ export class GetKeychainEntryV1Endpoint implements IWebServiceEndpoint { } public async handleRequest(req: Request, res: Response): Promise { + const { log } = this; const fnTag = `${this.className}#handleRequest()`; const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); @@ -96,7 +97,7 @@ export class GetKeychainEntryV1Endpoint implements IWebServiceEndpoint { res.json({ key, value }); } catch (ex) { const errorMsg = `${reqTag} ${fnTag} Failed to deploy contract:`; - handleRestEndpointException({ errorMsg, log: this.log, error: ex, res }); + await handleRestEndpointException({ errorMsg, log, error: ex, res }); } } } diff --git a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/has-keychain-entry-endpoint-v1.ts b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/has-keychain-entry-endpoint-v1.ts index 4b632f956a..8d0452a436 100644 --- a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/has-keychain-entry-endpoint-v1.ts +++ b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/has-keychain-entry-endpoint-v1.ts @@ -87,6 +87,7 @@ export class HasKeychainEntryV1Endpoint implements IWebServiceEndpoint { } public async handleRequest(req: Request, res: Response): Promise { + const { log } = this; const fnTag = `${this.className}#handleRequest()`; const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); @@ -103,7 +104,7 @@ export class HasKeychainEntryV1Endpoint implements IWebServiceEndpoint { res.json(resBody); } catch (ex) { const errorMsg = `${reqTag} ${fnTag} Failed to deploy contract:`; - handleRestEndpointException({ errorMsg, log: this.log, error: ex, res }); + await handleRestEndpointException({ errorMsg, log, error: ex, res }); } } } diff --git a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/set-keychain-entry-endpoint-v1.ts b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/set-keychain-entry-endpoint-v1.ts index fdf32faac6..743221d45c 100644 --- a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/set-keychain-entry-endpoint-v1.ts +++ b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/set-keychain-entry-endpoint-v1.ts @@ -87,6 +87,7 @@ export class SetKeychainEntryV1Endpoint implements IWebServiceEndpoint { } public async handleRequest(req: Request, res: Response): Promise { + const { log } = this; const fnTag = `${this.className}#handleRequest()`; const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); @@ -96,7 +97,7 @@ export class SetKeychainEntryV1Endpoint implements IWebServiceEndpoint { res.json(resBody); } catch (ex) { const errorMsg = `${reqTag} ${fnTag} Failed to deploy contract:`; - handleRestEndpointException({ errorMsg, log: this.log, error: ex, res }); + await handleRestEndpointException({ errorMsg, log, error: ex, res }); } } } diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/deploy-contract-solidity-bytecode-endpoint.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/deploy-contract-solidity-bytecode-endpoint.ts index 92b0d72e6b..5d14aaef7e 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/deploy-contract-solidity-bytecode-endpoint.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/deploy-contract-solidity-bytecode-endpoint.ts @@ -89,6 +89,7 @@ export class DeployContractSolidityBytecodeEndpoint } public async handleRequest(req: Request, res: Response): Promise { + const { log } = this; const fnTag = `${this.className}#handleRequest()`; const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); @@ -98,7 +99,7 @@ export class DeployContractSolidityBytecodeEndpoint res.json(resBody); } catch (ex) { const errorMsg = `${reqTag} ${fnTag} Failed to deploy contract:`; - handleRestEndpointException({ errorMsg, log: this.log, error: ex, res }); + await handleRestEndpointException({ errorMsg, log, error: ex, res }); } } } diff --git a/yarn.lock b/yarn.lock index 0f17b6cc47..abc0e6a168 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6814,6 +6814,8 @@ __metadata: express-jwt-authz: 2.4.1 express-openapi-validator: 5.0.4 http-errors: 2.0.0 + http-errors-enhanced: 1.1.2 + node-mocks-http: 1.14.0 run-time-error-cjs: 1.4.0 safe-stable-stringify: 2.4.3 typescript-optional: 2.0.1 @@ -14940,7 +14942,7 @@ __metadata: languageName: node linkType: hard -"accepts@npm:^1.3.5, accepts@npm:~1.3.7, accepts@npm:~1.3.8": +"accepts@npm:^1.3.5, accepts@npm:^1.3.7, accepts@npm:~1.3.7, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" dependencies: @@ -19651,7 +19653,7 @@ __metadata: languageName: node linkType: hard -"content-disposition@npm:0.5.4": +"content-disposition@npm:0.5.4, content-disposition@npm:^0.5.3": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" dependencies: @@ -21594,7 +21596,7 @@ __metadata: languageName: node linkType: hard -"depd@npm:^1.1.2, depd@npm:~1.1.2": +"depd@npm:^1.1.0, depd@npm:^1.1.2, depd@npm:~1.1.2": version: 1.1.2 resolution: "depd@npm:1.1.2" checksum: 6b406620d269619852885ce15965272b829df6f409724415e0002c8632ab6a8c0a08ec1f0bd2add05dc7bd7507606f7e2cc034fa24224ab829580040b835ecd9 @@ -26167,7 +26169,7 @@ __metadata: languageName: node linkType: hard -"fresh@npm:0.5.2": +"fresh@npm:0.5.2, fresh@npm:^0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" checksum: 13ea8b08f91e669a64e3ba3a20eb79d7ca5379a81f1ff7f4310d54e2320645503cc0c78daedc93dfb6191287295f6479544a649c64d8e41a1c0fb0c221552346 @@ -28216,6 +28218,13 @@ __metadata: languageName: node linkType: hard +"http-errors-enhanced@npm:1.1.2": + version: 1.1.2 + resolution: "http-errors-enhanced@npm:1.1.2" + checksum: 1bc38968cc4a0dcb6a2d25e557d0321f3fa17da2f1a6b898158fa3b9bb103fd5374b38e60d87926e5adf9310fd7b6136023754b0db1489a6e80d6d242375ba0f + languageName: node + linkType: hard + "http-errors@npm:1.6.3, http-errors@npm:~1.6.2": version: 1.6.3 resolution: "http-errors@npm:1.6.3" @@ -34443,6 +34452,13 @@ __metadata: languageName: node linkType: hard +"merge-descriptors@npm:^1.0.1": + version: 1.0.3 + resolution: "merge-descriptors@npm:1.0.3" + checksum: 52117adbe0313d5defa771c9993fe081e2d2df9b840597e966aadafde04ae8d0e3da46bac7ca4efc37d4d2b839436582659cd49c6a43eacb3fe3050896a105d1 + languageName: node + linkType: hard + "merge-options@npm:^3.0.4": version: 3.0.4 resolution: "merge-options@npm:3.0.4" @@ -34498,7 +34514,7 @@ __metadata: languageName: node linkType: hard -"methods@npm:~1.1.2": +"methods@npm:^1.1.2, methods@npm:~1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" checksum: 0917ff4041fa8e2f2fda5425a955fe16ca411591fbd123c0d722fcf02b73971ed6f764d85f0a6f547ce49ee0221ce2c19a5fa692157931cecb422984f1dcd13a @@ -34602,7 +34618,7 @@ __metadata: languageName: node linkType: hard -"mime@npm:1.6.0, mime@npm:^1.4.1": +"mime@npm:1.6.0, mime@npm:^1.3.4, mime@npm:^1.4.1": version: 1.6.0 resolution: "mime@npm:1.6.0" bin: @@ -35941,6 +35957,24 @@ __metadata: languageName: node linkType: hard +"node-mocks-http@npm:1.14.0": + version: 1.14.0 + resolution: "node-mocks-http@npm:1.14.0" + dependencies: + accepts: ^1.3.7 + content-disposition: ^0.5.3 + depd: ^1.1.0 + fresh: ^0.5.2 + merge-descriptors: ^1.0.1 + methods: ^1.1.2 + mime: ^1.3.4 + parseurl: ^1.3.3 + range-parser: ^1.2.0 + type-is: ^1.6.18 + checksum: 2cbf2d2e0dcbf6935e2f2c7e3b3bbea4b60cf043f8cf846856bf177e6570bf7b5629311b97f673d290cf51ed8e7e7c725f9a7a43ad224df423ed92389ac9ff8f + languageName: node + linkType: hard + "node-notifier@npm:8.0.2": version: 8.0.2 resolution: "node-notifier@npm:8.0.2" @@ -40375,7 +40409,7 @@ __metadata: languageName: node linkType: hard -"range-parser@npm:^1.2.1, range-parser@npm:~1.2.1": +"range-parser@npm:^1.2.0, range-parser@npm:^1.2.1, range-parser@npm:~1.2.1": version: 1.2.1 resolution: "range-parser@npm:1.2.1" checksum: 0a268d4fea508661cf5743dfe3d5f47ce214fd6b7dec1de0da4d669dd4ef3d2144468ebe4179049eff253d9d27e719c88dae55be64f954e80135a0cada804ec9 @@ -46647,7 +46681,7 @@ __metadata: languageName: node linkType: hard -"type-is@npm:^1.6.4, type-is@npm:~1.6.17, type-is@npm:~1.6.18": +"type-is@npm:^1.6.18, type-is@npm:^1.6.4, type-is@npm:~1.6.17, type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" dependencies: