UNPKG

urisanity

Version:

vet URIs in web and web-like applications with confidence

291 lines (238 loc) 12 kB
/*! * URISanity: Date=Tue Feb 15 2022 20:40:59 GMT+0100 (West Africa Standard Time);NodeBuildVersion=14 * */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.urisanity = factory()); }(this, function () { 'use strict'; /** ! * @author: https://twitter.com/isocroft * @owner: https://twitter.com/codesplinta * * @Copyright (c) 2021 - 2022 * * @sourced: [first-party] https://github.com/braintree/sanitize-url * * Based on the well known URI schemes; * See: https://en.wikipedia.org/wiki/List_of_URI_schemes * * @created: 23/06/2021 * @last-updated: 12/02/2022 */ /* eslint-disable no-useless-escape */ /* @HINT: all URI schemes that are mostly unsafe for web browsers to launch */ var unsafeURISchemeRegex = /^([^\w]*)(javascript|vbscript|app|admin|icloud-sharing|icloud-vetting|file|help|aim|facetime-audio|applefeedback|ibooks|macappstore|udoc|ts|st|x-apple-helpbasic|(?:x\-)?radar)/im; /* @HINT: all URI schemes that are mostly safe for web browsers to launch */ var safeInternetURISchemeRegex = /^(?:(?:f|ht)tps?|cid|xmpp|mms|webcal|aaa|acap|bolo|data|blob|wss?|irc|udp)/im; /* @CHECK: https://gist.github.com/gruber/249502/61cbb59f099fdf90316c4e409c7523b6d5124f80 */ var safeURIRegex = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/?)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\)){0,}(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s\!()\[\]{};:\'\"\.\,<>?«»“”‘’]){0,})/i; /* @HINT: */ var commsAppURISchemeRegex = /^(whatsapp|zoommtg|slack|mailto|tel|callto|sms|skype)/im; var databaseConnectionStringURISchemeRegex = /^(jdbc(:sqlserver|:mysql|:mariadb|:sqlite)?|odbc|postgres(ql)?|mongodb)/im; var browserURISchemeRegex = /^(view-source|moz-extension|chrome-extension)/im; var serviceAPIURISchemeRegex = /^(cloudinary|gs|s3|grpc)/im; /* @HINT: */ var ctrlCharactersRegex = /[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim; /* eslint-disable-line */ // const urlSchemeRegex = /^([^:]+):/gm /* @CHECK: https://datatracker.ietf.org/doc/html/rfc2397 - DATA URI */ /* @CHECK: https://www.w3.org/TR/FileAPI/#blob-url - BLOB URL */ var dataURIRegex = /^(?:data:([\w-.]+\/[\w-.]+(\+[\w-.]+)?)?(;[\w-.]+=[\w-.]+)*;base64,([a-zA-Z0-9\/+\n=]+))$/; var scriptURIRegex = /^(?:vb|java)script:/i; var webTransportURIRegex = /^(?:(blob:)?https?|wss?|about)/im; var relativeFirstCharacters = ['.', '/']; /* @HINT: Global Stub for the Browser, ReactNative, NativeScript, NodeJS */ var $globals = typeof self === 'undefined' ? global || {} : self; /* @HINT: Conditionally access the NodeJS process global */ var nodeJSProcess = $globals.process || { versions: { node: '.' }, env: {} }; var NODE_MAJOR_VERSION = parseInt(nodeJSProcess.versions.node.split('.')[0]); /* @CHECK: https://developer.mozilla.org/en-US/docs/Web/API/URL#browser_compatibility */ if (NODE_MAJOR_VERSION <= 12) { if (!$globals.URL) { $globals.URL = function (urlString) { var urlParser = require('url'); /* urlParser.parse(): deprecarted in NodeJS v11.x */ var parsedUrl = NODE_MAJOR_VERSION <= 11 ? urlParser.parse(urlString) /* eslint-disable-line */ : urlParser.URL(urlString); return Object.assign({}, parsedUrl, { hostname: parsedUrl.host }); }; } } function isStandardBrowserEnv() { var environ = $globals.navigator; return typeof environ !== 'undefined' && environ.product.match(/^(ReactNative|NativeScript|NS)$/i) === null; } var origin = isStandardBrowserEnv() ? $globals.location.origin : ($globals.constants || nodeJSProcess.env).ORIGIN; function isRelativeUrlWithoutProtocol(url) { if (typeof url === 'string') { return relativeFirstCharacters.indexOf(url.charAt(0)) > -1; } return false; } /* @CHECK: https://gist.github.com/blafrance/4053759 */ function extractParamValueFromUri(uri, paramName) { if (!uri) { return; } var regex = new RegExp('[\\?&#]' + paramName + '=([^&#]*)'); var params = regex.exec(uri); if (params != null) { return unescape(params[1]); } } function formDataToJSON(elem) { var current = void 0, item = void 0, key = void 0, output = void 0, value = void 0; output = {}; var entries = elem instanceof $globals.FormData ? elem.entries() : new $globals.FormData(elem).entries(); try { // Iterate over values, and assign to item. while ((item = entries.next().value) !== null) { // assign to variables to make the code more readable. key = item[0]; value = item[1]; // Check if key already exist if (Object.prototype.hasOwnProperty.call(output, key)) { current = output[key]; if (!Array.isArray(current)) { // If it's not an array, convert it to an array. current = output[key] = [current]; } current.push(value); // Add the new value to the array. } else { output[key] = value; } } } catch (_) { output = Object.fromEntries(entries); } return JSON.stringify(output); } function isSameOrigin(uri) { if (!uri) { return; } var parsedUrl = new $globals.URL(uri); return origin === parsedUrl.origin; } function checkParamsOverWhiteList(uri) { var paramsWhiteList = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; var data = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; var parsedUrl = new $globals.URL(uri); var paramKeys = []; var paramValues = []; var preparedData = null; try { var json = ''; if ('FormData' in $globals && data instanceof $globals.FormData) { if (typeof Object.fromEntries === 'function') { json = formDataToJSON(data); } else { var object = {}; data.forEach(function (value, key) { object[key] = value; }); json = JSON.stringify(object); } } else { json = data; } if (typeof json === 'string') { preparedData = JSON.parse(json); } } catch (_) { preparedData = data; } if (preparedData === '') { parsedUrl.searchParams.forEach(function () { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } var value = args[0], key = args[1]; paramValues.push(unescape(value)); paramKeys.push(key); }); } else { if (preparedData instanceof Object) { paramValues.concat(Object.values(preparedData)); } } /* @HINT: Check that only the request params we need are attached */ /* @HINT: Any other extra params should not be allowed */ if (paramKeys.length === paramsWhiteList.length) { return true; } return false; } function sanitizeUrl(url) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (!url || url.match(/:\/\/(?:[#$@=*.!]|[/]){0,}$/) !== null || url.includes('////////////')) { return 'about:blank'; } var sanitizedUrl = url.replace(ctrlCharactersRegex, '').trim(); if (isRelativeUrlWithoutProtocol(sanitizedUrl)) { var originalSanitized = sanitizedUrl.startsWith('/') ? origin + sanitizedUrl : origin + '/' + sanitizedUrl; sanitizedUrl = safeURIRegex.test(originalSanitized) ? originalSanitized : '//'; } var urlSchemeParseResults = sanitizedUrl.match(safeInternetURISchemeRegex) || sanitizedUrl.match(commsAppURISchemeRegex) || sanitizedUrl.match(databaseConnectionStringURISchemeRegex) || sanitizedUrl.match(browserURISchemeRegex); urlSchemeParseResults = urlSchemeParseResults !== null ? urlSchemeParseResults : []; var urlScheme = urlSchemeParseResults[0] || ''; var _ref = new $globals.URL(sanitizedUrl.toLowerCase()), hostname = _ref.hostname, pathname = _ref.pathname, search = _ref.search, hash = _ref.hash; /* @CHECK: https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names */ /* CHECK: https://datatracker.ietf.org/doc/html/rfc3986 */ if (/^(?:((?:www|[a-z]{1,11})\.)(?!\1)(?:[a-z\-\d]{1,63})\.(?:[a-z.\-\d]{2,63}))$/i.test(hostname) || /^(((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/.test(hostname) || /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/.test(hostname) || /^(?:((?:[a-z0-9-._~]{0,}|%[1-9a-f]?[0-9a-f]|[!$&'()*+,;=])|\:|\@)*)/i.test(pathname) || /^((?:[a-z0-9-._~]{0,}|%[1-9a-f]?[0-9a-f]|[!$&'()*+,;=])|\:|\@|\/|\?)*$/i.test(search) || /^((?:[a-z0-9-._~]{0,}|%[1-9a-f]?[0-9a-f]|[!$&'()*+,;=])|\:|\@|\/|\?)*$/i.test(hash)) { if (hostname.includes('.00')) { return 'about:blank'; } if (search.toLowerCase().match(/%3c(?=\/)?/g) !== null && search.toLowerCase().includes('%3e') && search.toLowerCase().includes('%3f') && search.toLowerCase().includes('%3d') && search.toLowerCase().includes('%27') && search.toLowerCase().includes('%22') && search.toLowerCase().match(/\.(?:jar|dmg|exe|bin|sh|sed)/g) !== null) { return 'about:blank'; } } if (!unsafeURISchemeRegex.test(urlScheme) || safeURIRegex.test(sanitizedUrl)) { var pass = false; if (options.allowScriptOrDataURI && urlScheme.match(/^(java|vb)script|data/) === null && (dataURIRegex.test(sanitizedUrl) || scriptURIRegex.test(sanitizedUrl))) { pass = true; } if (options.allowCommsAppURI && commsAppURISchemeRegex.test(sanitizedUrl)) { pass = true; } if (options.allowDBConnectionStringURI && databaseConnectionStringURISchemeRegex.test(sanitizedUrl)) { pass = true; } if (options.allowServiceAPIURI && serviceAPIURISchemeRegex.test(sanitizedUrl)) { pass = true; } if (options.allowBrowserSpecificURI && browserURISchemeRegex.test(sanitizedUrl)) { pass = true; } if (options.allowWebTransportURI && webTransportURIRegex.test(sanitizedUrl)) { pass = true; } if (urlScheme !== '' && pass) { return sanitizedUrl; } else { return 'about:blank'; // sanitizedUrl; } } } var URISanity = { isSameOrigin: isSameOrigin, extractParamValueFromUri: extractParamValueFromUri, checkParamsOverWhiteList: checkParamsOverWhiteList, vet: function vet(url) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; return sanitizeUrl(url, options || {}); } }; return URISanity; })); //# sourceMappingURL=urisanity.js.map