diff --git a/backend/package-lock.json b/backend/package-lock.json index 2f4e35e9..b041a984 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -598,6 +598,12 @@ "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", "dev": true }, + "@types/safe-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/safe-regex/-/safe-regex-1.1.2.tgz", + "integrity": "sha512-wuS9LVpgIiTYaGKd+s6Dj0kRXBkttaXjVxzaXmviCACi8RO+INPayND+VNjAcall/l1Jkyhh9lyPfKW/aP/Yug==", + "dev": true + }, "@types/serve-static": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", diff --git a/backend/package.json b/backend/package.json index 08e2a049..6342e440 100644 --- a/backend/package.json +++ b/backend/package.json @@ -67,6 +67,7 @@ "@types/passport": "^1.0.0", "@types/passport-oauth2": "^1.4.8", "@types/passport-strategy": "^0.2.35", + "@types/safe-regex": "^1.1.2", "@types/tmp": "0.0.33", "ava": "^2.4.0", "rimraf": "^2.6.2", diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 2b1f2e4c..5d88c29e 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -58,6 +58,8 @@ import LCL from "last-commit-log"; import * as t from "io-ts"; import { ICommandDefinition } from "knub-command-manager"; import path from "path"; +import escapeStringRegexp from "escape-string-regexp"; +import safeRegex from "safe-regex"; const ConfigSchema = t.type({ can_roles: t.boolean, @@ -97,8 +99,11 @@ type MemberSearchParams = { bot?: boolean; sort?: string; "case-sensitive"?: boolean; + regex?: boolean; }; +class SearchError extends Error {} + export class UtilityPlugin extends ZeppelinPlugin { public static pluginName = "utility"; public static configSchema = ConfigSchema; @@ -334,17 +339,22 @@ export class UtilityPlugin extends ZeppelinPlugin { } if (args.query) { - const query = args["case-sensitive"] ? args.query.trimStart() : args.query.toLowerCase().trimStart(); + let queryRegex: RegExp; + if (args.regex) { + queryRegex = new RegExp(args.query.trimStart(), args["case-sensitive"] ? "i" : ""); + } else { + queryRegex = new RegExp(escapeStringRegexp(args.query.trimStart()), args["case-sensitive"] ? "i" : ""); + } + + if (!safeRegex(queryRegex)) { + throw new SearchError("Unsafe/too complex regex (star depth is limited to 1)"); + } matchingMembers = matchingMembers.filter(member => { - const nick = args["case-sensitive"] ? member.nick : member.nick && member.nick.toLowerCase(); + if (member.nick && member.nick.match(queryRegex)) return true; - const fullUsername = args["case-sensitive"] - ? `${member.user.username}#${member.user.discriminator}` - : `${member.user.username}#${member.user.discriminator}`.toLowerCase(); - - if (nick && nick.indexOf(query) !== -1) return true; - if (fullUsername.indexOf(query) !== -1) return true; + const fullUsername = `${member.user.username}#${member.user.discriminator}`; + if (fullUsername.match(queryRegex)) return true; return false; }); @@ -423,6 +433,11 @@ export class UtilityPlugin extends ZeppelinPlugin { name: "ids", isSwitch: true, }, + { + name: "regex", + shortcut: "re", + isSwitch: true, + }, ], extra: { info: { @@ -453,6 +468,7 @@ export class UtilityPlugin extends ZeppelinPlugin { "case-sensitive"?: boolean; export?: boolean; ids?: boolean; + regex?: boolean; }, ) { const formatSearchResultList = (members: Member[]): string => { @@ -473,7 +489,17 @@ export class UtilityPlugin extends ZeppelinPlugin { // If we're exporting the results, we don't need all the fancy schmancy pagination stuff. // Just get the results and dump them in an archive. if (args.export) { - const results = await this.performMemberSearch(args, 1, SEARCH_EXPORT_LIMIT); + let results; + try { + results = await this.performMemberSearch(args, 1, SEARCH_EXPORT_LIMIT); + } catch (e) { + if (e instanceof SearchError) { + return this.sendErrorMessage(msg.channel, e.message); + } + + throw e; + } + if (results.totalResults === 0) { return this.sendErrorMessage(msg.channel, "No results found"); } @@ -519,7 +545,17 @@ export class UtilityPlugin extends ZeppelinPlugin { searchMsgPromise.then(m => (originalSearchMsg = m)); } - const searchResult = await this.performMemberSearch(args, page, perPage); + let searchResult; + try { + searchResult = await this.performMemberSearch(args, page, perPage); + } catch (e) { + if (e instanceof SearchError) { + return this.sendErrorMessage(msg.channel, e.message); + } + + throw e; + } + if (searchResult.totalResults === 0) { return this.sendErrorMessage(msg.channel, "No results found"); }