From b8a97cffcff544e247716c65e63dc7e8a74f418c Mon Sep 17 00:00:00 2001 From: Fares Osman <43153226+fiosman@users.noreply.github.com> Date: Fri, 15 May 2026 12:56:14 -0400 Subject: [PATCH] feat: adds proper error messages when user attempts to upload a rule set with severity tuples in extends --- .../src/common/spectral-ruleset-validator.ts | 20 ++++++++++++------- .../src/main/bundle-spectral-ruleset.ts | 5 +++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/insomnia/src/common/spectral-ruleset-validator.ts b/packages/insomnia/src/common/spectral-ruleset-validator.ts index 70ab5cad58..c361373f33 100644 --- a/packages/insomnia/src/common/spectral-ruleset-validator.ts +++ b/packages/insomnia/src/common/spectral-ruleset-validator.ts @@ -44,10 +44,11 @@ export function isLocalFilePath(value: string): boolean { } export function toArray(value: T | T[] | undefined): T[] { + //no extends key in the ruleset if (value === undefined) { return []; } - return Array.isArray(value) ? value : [value]; + return Array.isArray(value) ? value : [value]; // handles both array and single value cases for extends in a given ruleset } // Given our support for remote extends, we need to protect against the possibility of SSRF attacks. We block any hostname that is a loopback or private network address, as well as "localhost". @@ -99,29 +100,34 @@ function validateThen(ruleName: string, then: Record): string | function validateExtends(value: unknown): string | null { for (const entry of toArray(value)) { - if (typeof entry !== 'string') { + if (Array.isArray(entry)) { + return `"extends" entry ${JSON.stringify(entry)} uses tuple format (e.g. [path, severity]) which is not supported. Use a plain string instead.`; + } + + const path = entry; + if (typeof path !== 'string') { return '"extends" entries must be strings.'; } // allow built in identifier and local file paths without further validation - if (ALLOWED_EXTENDS_IDENTIFIERS.includes(entry) || isLocalFilePath(entry)) { + if (ALLOWED_EXTENDS_IDENTIFIERS.includes(path) || isLocalFilePath(path)) { continue; } // validate remote URLs let url: URL; try { - url = new URL(entry); + url = new URL(path); } catch { - return `"extends" entry "${entry}" is not a recognized Spectral identifier or a valid URL.`; + return `"extends" entry "${path}" is not a recognized Spectral identifier or a valid URL.`; } if (!SAFE_URL_SCHEMES.includes(url.protocol)) { - return `"extends" entry "${entry}" must use https (got "${url.protocol}").`; + return `"extends" entry "${path}" must use https (got "${url.protocol}").`; } if (!url.hostname || isPrivateOrLoopbackHost(url.hostname.toLocaleLowerCase())) { - return `"extends" entry "${entry}" targets a disallowed host`; + return `"extends" entry "${path}" targets a disallowed host`; } } return null; diff --git a/packages/insomnia/src/main/bundle-spectral-ruleset.ts b/packages/insomnia/src/main/bundle-spectral-ruleset.ts index 724586aeba..86be5aa619 100644 --- a/packages/insomnia/src/main/bundle-spectral-ruleset.ts +++ b/packages/insomnia/src/main/bundle-spectral-ruleset.ts @@ -83,6 +83,11 @@ async function flattenRuleset(filePath: string, visited: Set, depth: num // - keep them in a separate list // - include them later in the final "extends" array for (const entry of toArray(ruleset.extends)) { + if (Array.isArray(entry)) { + throw new Error( + `Failed to process "extends" entry ${JSON.stringify(entry)}: tuple format (e.g. [path, severity]) is not supported. Use a plain string instead.`, + ); + } // If this entry is NOT a local file path, // keep it as-is for the final output. if (!isLocalFilePath(entry)) {