From 9c0928322cb3f409d6781401da9a8cc27f86c3a7 Mon Sep 17 00:00:00 2001 From: MattIPv4 Date: Wed, 3 Jan 2024 19:51:20 +0000 Subject: [PATCH] Switch to using cidr-matcher for SPF IP eval --- package-lock.json | 132 +++++++++++++++++++++++++ package.json | 2 + src/spf-explainer/utils/spf_sandbox.ts | 63 +++--------- tsconfig.json | 1 + 4 files changed, 148 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index ee16b64..1be64f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "abortcontroller-polyfill": "^1.7.5", "babel-polyfill": "^6.26.0", + "cidr-matcher": "^2.1.1", "do-bulma": "github:do-community/do-bulma", "do-vue": "github:do-community/do-vue", "vue": "^3.3.4", @@ -18,6 +19,7 @@ "web-whois": "0.0.6" }, "devDependencies": { + "@types/cidr-matcher": "^2.1.2", "@typescript-eslint/eslint-plugin": "^6.2.1", "@typescript-eslint/parser": "^6.2.1", "@vue/component-compiler-utils": "^3.3.0", @@ -2612,6 +2614,12 @@ "@types/node": "*" } }, + "node_modules/@types/cidr-matcher": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cidr-matcher/-/cidr-matcher-2.1.2.tgz", + "integrity": "sha512-tuyfj7hfDCBolTk+TDPC6OA6YlKIS3dpvYDGNKzRMVq/wdrl6LD5MDqRoVKPhJ+4nuOrBz2yKnzclzy7t28NyA==", + "dev": true + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -3532,6 +3540,14 @@ "util": "^0.12.0" } }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -4096,6 +4112,14 @@ "node": ">=8" } }, + "node_modules/cidr-matcher": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cidr-matcher/-/cidr-matcher-2.1.1.tgz", + "integrity": "sha512-QPJRz4HDQxpB8AZWEqd6ejVp+siArXh3u1MYaUFV85cd293StGSMb87jVe0z9gS92KsFwxCxjb3utO3e5HKHTw==", + "dependencies": { + "ip6addr": "^0.2.2" + } + }, "node_modules/clean-css": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", @@ -5864,6 +5888,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ] + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6932,6 +6964,15 @@ "node": ">= 0.4" } }, + "node_modules/ip6addr": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/ip6addr/-/ip6addr-0.2.5.tgz", + "integrity": "sha512-9RGGSB6Zc9Ox5DpDGFnJdIeF0AsqXzdH+FspCfPPaU/L/4tI6P+5lIoFUFm9JXs9IrJv1boqAaNCQmoDADTSKQ==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2" + } + }, "node_modules/ipaddr.js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", @@ -7499,6 +7540,11 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7521,6 +7567,20 @@ "node": ">=6" } }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -11819,6 +11879,19 @@ "node": ">= 0.8" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "node_modules/vue": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", @@ -14331,6 +14404,12 @@ "@types/node": "*" } }, + "@types/cidr-matcher": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cidr-matcher/-/cidr-matcher-2.1.2.tgz", + "integrity": "sha512-tuyfj7hfDCBolTk+TDPC6OA6YlKIS3dpvYDGNKzRMVq/wdrl6LD5MDqRoVKPhJ+4nuOrBz2yKnzclzy7t28NyA==", + "dev": true + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -15090,6 +15169,11 @@ "util": "^0.12.0" } }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" + }, "astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -15486,6 +15570,14 @@ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==" }, + "cidr-matcher": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cidr-matcher/-/cidr-matcher-2.1.1.tgz", + "integrity": "sha512-QPJRz4HDQxpB8AZWEqd6ejVp+siArXh3u1MYaUFV85cd293StGSMb87jVe0z9gS92KsFwxCxjb3utO3e5HKHTw==", + "requires": { + "ip6addr": "^0.2.2" + } + }, "clean-css": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", @@ -16789,6 +16881,11 @@ } } }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -17543,6 +17640,15 @@ "side-channel": "^1.0.4" } }, + "ip6addr": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/ip6addr/-/ip6addr-0.2.5.tgz", + "integrity": "sha512-9RGGSB6Zc9Ox5DpDGFnJdIeF0AsqXzdH+FspCfPPaU/L/4tI6P+5lIoFUFm9JXs9IrJv1boqAaNCQmoDADTSKQ==", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2" + } + }, "ipaddr.js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", @@ -17913,6 +18019,11 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -17929,6 +18040,17 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" }, + "jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -21037,6 +21159,16 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "vue": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", diff --git a/package.json b/package.json index 9072bd6..2b91c12 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "dependencies": { "abortcontroller-polyfill": "^1.7.5", "babel-polyfill": "^6.26.0", + "cidr-matcher": "^2.1.1", "do-bulma": "github:do-community/do-bulma", "do-vue": "github:do-community/do-vue", "vue": "^3.3.4", @@ -46,6 +47,7 @@ "web-whois": "0.0.6" }, "devDependencies": { + "@types/cidr-matcher": "^2.1.2", "@typescript-eslint/eslint-plugin": "^6.2.1", "@typescript-eslint/parser": "^6.2.1", "@vue/component-compiler-utils": "^3.3.0", diff --git a/src/spf-explainer/utils/spf_sandbox.ts b/src/spf-explainer/utils/spf_sandbox.ts index f645684..141aa35 100644 --- a/src/spf-explainer/utils/spf_sandbox.ts +++ b/src/spf-explainer/utils/spf_sandbox.ts @@ -14,53 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ +import CIDRMatcher from "cidr-matcher" + // Defines a SPF rule. class SPFRule { // Defines public values in the class. public rule: boolean | undefined - public range: RegExp - - // Turns the IP range into a regex. - private _regexFromString(range: string) { - const regexParts: string[] = [] - let regex = "" - range = range.split("/").shift()! - if (range.includes(".")) { - // This is a IPv4 address. - const partSplit = range.split(".") - for (const part of partSplit) { - if (part === "0") regexParts.push("[0-9]+") - else regexParts.push(part) - } - regex = `${regexParts.join("\\.")}.*` - } else { - // This is a IPv6 address. - let end: string | undefined - const parts: string[] = [] - - if (range.startsWith("::")) { - range = range.slice(2) - parts.push(".+") - } - - if (range.endsWith("::")) { - range = range.slice(0, -2) - end = ".+" - } - - parts.push(...range.split(":")) - - if (end) parts.push(end) - - regex = `${parts.join(":")}.*` - } - return RegExp(regex) - } + public matcher: CIDRMatcher // Constructs the rule. - public constructor(rule: boolean | undefined, range: string) { + public constructor(rule: boolean | undefined, ips: string[]) { this.rule = rule - this.range = this._regexFromString(range) + this.matcher = new CIDRMatcher(ips) } } @@ -88,23 +53,21 @@ class SPFSandbox { const ips = new Set() for (const v4 of spf.ip4 || []) ips.add(v4[0][1]) for (const v6 of spf.ip6 || []) ips.add(v6[0][1]) - for (const p of ips) this._rules.push(new SPFRule(action, p)) + this._rules.push(new SPFRule(action, [...ips])) this._listeners.forEach(listener => listener()) } // Evals the IP address/range given. + // Returns null if the IP is allowed, true for a hard fail, false for a soft fail, undefined for a neutral fail. public eval(ip: string) { - const rulesCpy: SPFRule[] = [] - for (const r of this._rules) rulesCpy.push(r) - - let hardfail: undefined | boolean - for (let ruleIndex = 0; rulesCpy.length > ruleIndex; ruleIndex++) { - const rule = rulesCpy[ruleIndex] - if (ip.match(rule.range)) return null - else hardfail = rule.rule + let hardFail: undefined | boolean + + for (const rule of this._rules) { + if (rule.matcher.contains(ip)) return null + hardFail = rule.rule } - return hardfail + return hardFail } // Listen for imports. diff --git a/tsconfig.json b/tsconfig.json index 95df548..e86e687 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "module": "commonjs", "target": "es2018", "allowJs": true, + "esModuleInterop": true, "noImplicitAny": true, "noEmit": false, "strict": true