UNPKG

urisanity

Version:

vet URIs in web and web-like applications with confidence

353 lines (291 loc) 15.6 kB
/*! * URISanity: Date=Tue Mar 05 2024 02:34:21 GMT+0100 (West Africa Standard Time);NodeBuildVersion=18 * */ (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'; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; /** ! * @author: https://twitter.com/isocroft * @owner: https://twitter.com/codesplinta * * @Copyright (c) 2021 - 2024 * * @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: 05/03/2024 */ /* eslint-disable no-useless-escape */ /* @HINT: all URI schemes that are mostly unsafe for web browsers to launch */ var unsafeURISchemeRegex = /^([^\w]*)(unsafe|javascript|vbscript|app|admin|icloud-sharing|icloud-vetting|help|aim|facetime-audio|applefeedback|ibooks|macappstore|udoc|ts|st|jar|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|file|local|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|resource|res|symres|(?:filesystem:|blob:)?chrome-extension|safari|chrome|mxaddon-pkg|mx|mxwebcore|mxjscall|mbinit|safari-resource|opera|webviewprogressproxy|chromenull|chromeinvoke|chromeinvokeimmediate)/im; /* @CHECK: - FILE URI */ /* @CHECK: https://www.w3.org/TR/FileAPI/#blob-url - BLOB URL */ var fileSystemURISchemeRegex = /^((?:jar:)?file|local|blob)/im; var serviceAPIURISchemeRegex = /^(cloudinary|obsidian|gs|s3|grpc|pptr|tmtbff)/im; /* @HINT: */ var blockedHosts = ['widgets.amung.us', 'v.zilionfast.in', 'js.blinkadr.com', 'www.superfish.com', 'nzj.divdriver.net', 'istatic.datafastguru.info', 'widgets.amung.us', 'xls.searchfun.in', 'static.image2play.com']; /* @HINT: All control characters */ 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 */ var dataURIBINRegex = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp|svg\+xml)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,(?:[a-zA-Z0-9\/+\n=]+)$/i; var dataURITEXTRegex = /^data:(?:text\/(?:javascript|html|css|plain))(?:;charset=(UTF-8|iso-8859-7))?,(?:.*)$/im; var scriptURIRegex = /^(?:vb|java)(?:\s*)?script/im; 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 || dataURITEXTRegex.test(uri.trim()) || dataURIBINRegex.test(uri.trim())) { return null; } var regex = new RegExp('[\\?&#]' + paramName + '=([^&#]*)'); var params = regex.exec(uri.trim()); 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 || dataURITEXTRegex.test(uri.trim()) || dataURIBINRegex.test(uri.trim())) { return false; } try { var parsedUrl = new $globals.URL(uri.trim()); return origin === parsedUrl.origin; } catch (error) { if (error) { return false; } } } function checkParamsOverWhiteList(uri) { var paramsWhiteList = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; var data = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; if (!uri || dataURITEXTRegex.test(uri.trim()) || dataURIBINRegex.test(uri.trim())) { return false; } if ((typeof paramsWhiteList === 'undefined' ? 'undefined' : _typeof(paramsWhiteList)) !== 'object') { return false; } var parsedUrl = new $globals.URL(uri.trim()); 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)); } } if (paramKeys.length === paramValues.length) { if (paramsWhiteList instanceof Array) { /* @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 && paramKeys.slice(0).sort().join('|') === paramsWhiteList.slice(0).sort().join('|')) { return true; } } else if (paramsWhiteList instanceof Object) { var paramsCounter = 0; for (; paramsCounter < paramKeys.length; paramsCounter++) { var paramKey = paramKeys[paramsCounter]; var paramValue = paramValues[paramsCounter]; var paramRegex = paramsWhiteList[paramKey]; if (paramRegex instanceof RegExp) { if (!paramRegex.test(paramValue)) { break; } } else { throw new Error('"' + paramKey + '" does not have a matching regex pattern to match "' + paramValue + '"'); } } if (paramsCounter === paramValues.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) || sanitizedUrl.match(serviceAPIURISchemeRegex) || sanitizedUrl.match(webTransportURIRegex); urlSchemeParseResults = urlSchemeParseResults !== null ? urlSchemeParseResults : []; var urlScheme = urlSchemeParseResults[0] || ''; try { 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') || blockedHosts.indexOf(hostname) !== -1) { return 'about:blank'; } if (pathname.toLowerCase().includes('&apos;') || pathname.toLowerCase().includes('%29') || pathname.toLowerCase().includes('%28') || pathname.toLowerCase().includes('%20') || pathname.toLowerCase().includes('%22') || pathname.toLowerCase().includes('&quot;') || pathname.toLowerCase().includes('(') || pathname.toLowerCase().includes(')') || pathname.toLowerCase().includes('%3e') || pathname.toLowerCase().includes('%3c') || pathname.toLowerCase().includes('><')) { 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().includes('&apos;') || search.toLowerCase().includes('&quot;') || search.toLowerCase().match(/\.(?:jar|dmg|exe|bin|sh|sed|py)/g) !== null) { return 'about:blank'; } } } catch (error) { if (error) { /* console.warn(`WARNING: unsafe URL > ${sanitizedUrl} (see https://g.co/ng/security#xss)`) */ return 'about:blank'; } } if (!unsafeURISchemeRegex.test(urlScheme) || safeURIRegex.test(sanitizedUrl)) { var pass = false; if (options.allowScriptOrDataURI && urlScheme.match(/^(java|vb)script|data/) !== null && (dataURIBINRegex.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 (options.allowFileSystemURI && fileSystemURISchemeRegex.test(sanitizedUrl)) { pass = true; } if (urlScheme !== '' && pass) { return sanitizedUrl; } else { /* console.warn(`WARNING: unsafe URL > ${sanitizedUrl} (see https://g.co/ng/security#xss)`) */ return 'about:blank'; } } } 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