urisanity
Version:
vet URIs in web and web-like applications with confidence
353 lines (291 loc) • 15.6 kB
JavaScript
/*!
* 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(''') || pathname.toLowerCase().includes('%29') || pathname.toLowerCase().includes('%28') || pathname.toLowerCase().includes('%20') || pathname.toLowerCase().includes('%22') || pathname.toLowerCase().includes('"') || 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(''') || search.toLowerCase().includes('"') || 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