diff options
Diffstat (limited to 'comm/mail/components/accountcreation/Sanitizer.jsm')
-rw-r--r-- | comm/mail/components/accountcreation/Sanitizer.jsm | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/comm/mail/components/accountcreation/Sanitizer.jsm b/comm/mail/components/accountcreation/Sanitizer.jsm new file mode 100644 index 0000000000..d6bc3918bc --- /dev/null +++ b/comm/mail/components/accountcreation/Sanitizer.jsm @@ -0,0 +1,249 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["Sanitizer"]; + +const { AccountCreationUtils } = ChromeUtils.import( + "resource:///modules/accountcreation/AccountCreationUtils.jsm" +); + +const { cleanUpHostName, isLegalHostNameOrIP } = ChromeUtils.import( + "resource:///modules/hostnameUtils.jsm" +); + +/** + * This is a generic input validation lib. Use it when you process + * data from the network. + * + * Just a few functions which verify, for security purposes, that the + * input variables (strings, if nothing else is noted) are of the expected + * type and syntax. + * + * The functions take a string (unless noted otherwise) and return + * the expected datatype in JS types. If the value is not as expected, + * they throw exceptions. + */ + +// To debug, set mail.setup.loglevel="All" and kDebug = true. +var kDebug = false; + +var Sanitizer = { + integer(unchecked) { + if (typeof unchecked == "number" && !isNaN(unchecked)) { + return unchecked; + } + + var r = parseInt(unchecked); + if (isNaN(r)) { + throw new MalformedException("no_number.error", unchecked); + } + + return r; + }, + + integerRange(unchecked, min, max) { + var int = this.integer(unchecked); + if (int < min) { + throw new MalformedException("number_too_small.error", unchecked); + } + + if (int > max) { + throw new MalformedException("number_too_large.error", unchecked); + } + + return int; + }, + + boolean(unchecked) { + if (typeof unchecked == "boolean") { + return unchecked; + } + + if (unchecked == "true") { + return true; + } + + if (unchecked == "false") { + return false; + } + + throw new MalformedException("boolean.error", unchecked); + }, + + string(unchecked) { + return String(unchecked); + }, + + nonemptystring(unchecked) { + if (!unchecked) { + throw new MalformedException("string_empty.error", unchecked); + } + + return this.string(unchecked); + }, + + /** + * Allow only letters, numbers, "-" and "_". + * + * Empty strings not allowed (good idea?). + */ + alphanumdash(unchecked) { + var str = this.nonemptystring(unchecked); + if (!/^[a-zA-Z0-9\-\_]*$/.test(str)) { + throw new MalformedException("alphanumdash.error", unchecked); + } + + return str; + }, + + /** + * DNS hostnames like foo.bar.example.com + * Allow only letters, numbers, "-" and "." + * Empty strings not allowed. + * Currently does not support IDN (international domain names). + */ + hostname(unchecked) { + let str = cleanUpHostName(this.nonemptystring(unchecked)); + + // Allow placeholders. TODO move to a new hostnameOrPlaceholder() + // The regex is "anything, followed by one or more (placeholders than + // anything)". This doesn't catch the non-placeholder case, but that's + // handled down below. + if (/^[a-zA-Z0-9\-\.]*(%[A-Z0-9]+%[a-zA-Z0-9\-\.]*)+$/.test(str)) { + return str; + } + + if (!isLegalHostNameOrIP(str)) { + throw new MalformedException("hostname_syntax.error", unchecked); + } + + return str.toLowerCase(); + }, + + /** + * A value which resembles an email address. + */ + emailAddress(unchecked) { + let str = this.nonemptystring(unchecked); + if (!/^[a-z0-9\-%+_\.\*]+@[a-z0-9\-\.]+\.[a-z]+$/i.test(str)) { + throw new MalformedException("emailaddress_syntax.error", unchecked); + } + + return str.toLowerCase(); + }, + + /** + * A non-chrome URL that's safe to request. + */ + url(unchecked) { + var str = this.string(unchecked); + + // DANGER ZONE: data:text/javascript or data:text/html can contain + // JavaScript code, run in the caller's security context, and might allow + // arbitrary code execution, so these must be prevented at all costs. + // PNG and JPEG data: URLs are fine. But SVG is again dangerous, + // it can contain javascript, so it would create a critical security hole. + // Talk to BenB or bz before relaxing *any* of the checks in this function. + if ( + str.startsWith("data:image/png;") || + str.startsWith("data:image/jpeg;") + ) { + return new URL(str).href; + } + + if (!str.startsWith("http:") && !str.startsWith("https:")) { + throw new MalformedException("url_scheme.error", unchecked); + } + + var uri; + try { + uri = Services.io.newURI(str); + uri = uri.QueryInterface(Ci.nsIURL); + } catch (e) { + throw new MalformedException("url_parsing.error", unchecked); + } + + if (uri.scheme != "http" && uri.scheme != "https") { + throw new MalformedException("url_scheme.error", unchecked); + } + + return uri.spec; + }, + + /** + * A value which should be shown to the user in the UI as label + */ + label(unchecked) { + return this.string(unchecked); + }, + + /** + * Allows only certain values as input, otherwise throw. + * + * @param unchecked {Any} The value to check + * @param allowedValues {Array} List of values that |unchecked| may have. + * @param defaultValue {Any} (Optional) If |unchecked| does not match + * anything in |mapping|, a |defaultValue| can be returned instead of + * throwing an exception. The latter is the default and happens when + * no |defaultValue| is passed. + * @throws MalformedException + */ + enum(unchecked, allowedValues, defaultValue) { + for (let allowedValue of allowedValues) { + if (allowedValue == unchecked) { + return allowedValue; + } + } + // value is bad + if (typeof defaultValue == "undefined") { + throw new MalformedException("allowed_value.error", unchecked); + } + return defaultValue; + }, + + /** + * Like enum, allows only certain (string) values as input, but allows the + * caller to specify another value to return instead of the input value. E.g., + * if unchecked == "foo", return 1, if unchecked == "bar", return 2, + * otherwise throw. This allows to translate string enums into integer enums. + * + * @param unchecked {Any} The value to check + * @param mapping {Object} Associative array. property name is the input + * value, property value is the output value. E.g. the example above + * would be: { foo: 1, bar : 2 }. + * Use quotes when you need freaky characters: "baz-" : 3. + * @param defaultValue {Any} (Optional) If |unchecked| does not match + * anything in |mapping|, a |defaultValue| can be returned instead of + * throwing an exception. The latter is the default and happens when + * no |defaultValue| is passed. + * @throws MalformedException + */ + translate(unchecked, mapping, defaultValue) { + for (var inputValue in mapping) { + if (inputValue == unchecked) { + return mapping[inputValue]; + } + } + // value is bad + if (typeof defaultValue == "undefined") { + throw new MalformedException("allowed_value.error", unchecked); + } + return defaultValue; + }, +}; + +function MalformedException(msgID, uncheckedBadValue) { + var stringBundle = AccountCreationUtils.getStringBundle( + "chrome://messenger/locale/accountCreationUtil.properties" + ); + var msg = stringBundle.GetStringFromName(msgID); + if (typeof kDebug != "undefined" && kDebug) { + msg += " (bad value: " + uncheckedBadValue + ")"; + } + AccountCreationUtils.Exception.call(this, msg); +} +MalformedException.prototype = Object.create( + AccountCreationUtils.Exception.prototype +); +MalformedException.prototype.constructor = MalformedException; |