urisanity
Version:
vet URIs in web and web-like applications with confidence
291 lines (238 loc) • 12 kB
JavaScript
/*!
* 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