/*! * @esri/arcgis-html-sanitizer - v3.0.1 - Tue Nov 15 2022 09:46:54 GMT-0800 (Pacific Standard Time) * Copyright (c) 2022 - Environmental Systems Research Institute, Inc. * Apache-2.0 * * js-xss * Copyright (c) 2012-2018 Zongmin Lei(雷宗民) * http://ucdok.com * MIT License, see https://github.com/leizongmin/js-xss/blob/master/LICENSE for details */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var xss = require('xss'); function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var xss__namespace = /*#__PURE__*/_interopNamespace(xss); /** * Determine if the value is a plain object. * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. */ var isPlainObject = function (value) { if (typeof value !== "object" || value === null) { return false; } if (Object.prototype.toString.call(value) !== "[object Object]") { return false; } var proto = Object.getPrototypeOf(value); if (proto === null) { return true; } while (Object.getPrototypeOf(proto) !== null) { proto = Object.getPrototypeOf(proto); } return Object.getPrototypeOf(value) === proto; }; /* Copyright (c) 2020 Environmental Systems Research Institute, Inc. * Apache-2.0 * * js-xss * Copyright (c) 2012-2018 Zongmin Lei(雷宗民) * http://ucdok.com * The MIT License, see * https://github.com/leizongmin/js-xss/blob/master/LICENSE for details * */ /** * The Sanitizer Class * * @export * @class Sanitizer */ var Sanitizer = /** @class */ (function () { function Sanitizer(filterOptions, extendDefaults) { var _this = this; // Supported HTML Spec: https://doc.arcgis.com/en/arcgis-online/reference/supported-html.htm this.arcgisWhiteList = { a: ["href", "style", "target"], abbr: ["title"], audio: ["autoplay", "controls", "loop", "muted", "preload"], b: [], br: [], dd: ["style"], div: ["align", "style"], dl: ["style"], dt: ["style"], em: [], figcaption: ["style"], figure: ["style"], font: ["color", "face", "size", "style"], h1: ["style"], h2: ["style"], h3: ["style"], h4: ["style"], h5: ["style"], h6: ["style"], hr: [], i: [], img: ["alt", "border", "height", "src", "style", "width"], li: [], ol: [], p: ["style"], source: ["media", "src", "type"], span: ["style"], strong: [], sub: ["style"], sup: ["style"], table: ["border", "cellpadding", "cellspacing", "height", "style", "width"], tbody: [], tr: ["align", "height", "style", "valign"], td: [ "align", "colspan", "height", "nowrap", "rowspan", "style", "valign", "width", ], th: [ "align", "colspan", "height", "nowrap", "rowspan", "style", "valign", "width", ], u: [], ul: [], video: [ "autoplay", "controls", "height", "loop", "muted", "poster", "preload", "width", ], }; this.allowedProtocols = [ "http", "https", "mailto", "iform", "tel", "flow", "lfmobile", "arcgis-navigator", "arcgis-appstudio-player", "arcgis-survey123", "arcgis-collector", "arcgis-workforce", "arcgis-explorer", "arcgis-trek2there", "arcgis-quickcapture", "mspbi", "comgooglemaps", "pdfefile", "pdfehttp", "pdfehttps", "boxapp", "boxemm", "awb", "awbs", "gropen", "radarscope", ]; this.arcgisFilterOptions = { allowCommentTag: true, safeAttrValue: function (tag, name, value, cssFilter) { // Take over safe attribute filtering for `a` `href`, `img` `src`, // and `source` `src` attributes, otherwise pass onto the // default `XSS.safeAttrValue` method. if ((tag === "a" && name === "href") || ((tag === "img" || tag === "source") && name === "src")) { return _this.sanitizeUrl(value); } return xss__namespace.safeAttrValue(tag, name, value, cssFilter); }, }; this._entityMap = { "&": "8", "<": "<", ">": ">", '"': """, "'": "'", "/": "/", }; var xssFilterOptions; if (filterOptions && !extendDefaults) { // Override the defaults xssFilterOptions = filterOptions; } else if (filterOptions && extendDefaults) { // Extend the defaults xssFilterOptions = Object.create(this.arcgisFilterOptions); Object.keys(filterOptions).forEach(function (key) { if (key === "whiteList") { // Extend the whitelist by concatenating arrays xssFilterOptions.whiteList = _this._extendObjectOfArrays([ _this.arcgisWhiteList, filterOptions.whiteList || {}, ]); } else { xssFilterOptions[key] = filterOptions[key]; } }); } else { // Only use the defaults xssFilterOptions = Object.create(this.arcgisFilterOptions); xssFilterOptions.whiteList = this.arcgisWhiteList; } this.xssFilterOptions = xssFilterOptions; // Make this readable to tests this._xssFilter = new xss__namespace.FilterXSS(xssFilterOptions); } /** * Sanitizes value to remove invalid HTML tags. * * Note: If the value passed does not contain a valid JSON data type (String, * Number, JSON Object, Array, Boolean, or null), the value will be nullified. * * @param {any} value The value to sanitize. * @returns {any} The sanitized value. * @memberof Sanitizer */ Sanitizer.prototype.sanitize = function (value, options) { if (options === void 0) { options = {}; } switch (typeof value) { case "number": if (isNaN(value) || !isFinite(value)) { return null; } return value; case "boolean": return value; case "string": return this._xssFilter.process(value); case "object": return this._iterateOverObject(value, options); default: if (options.allowUndefined && typeof value === "undefined") { return; } return null; } }; /** * Sanitizes a URL string following the allowed protocols and sanitization rules. * * @param {string} value The URL to sanitize. * @param {{ isProtocolRequired: boolean }} options Configuration options for URL checking. * @returns {string} The sanitized URL if it's valid, or an empty string if the URL is invalid. */ Sanitizer.prototype.sanitizeUrl = function (value, options) { var _a = (options !== null && options !== void 0 ? options : {}).isProtocolRequired, isProtocolRequired = _a === void 0 ? true : _a; var protocol = this._trim(value.substring(0, value.indexOf(":"))); var isRootUrl = value === '/'; var isUrlFragment = /^#/.test(value); var isValidProtocol = protocol && this.allowedProtocols.indexOf(protocol.toLowerCase()) > -1; if (isRootUrl || isUrlFragment || isValidProtocol) { return xss__namespace.escapeAttrValue(value); } if (!protocol && !isProtocolRequired) { return xss__namespace.escapeAttrValue("https://".concat(value)); } return ""; }; /** * Sanitizes an HTML attribute value. * * @param {string} tag The tagname of the HTML element. * @param {string} attribute The attribute name of the HTML element. * @param {string} value The raw value to be used for the HTML attribute value. * @param {XSS.ICSSFilter} [cssFilter] The CSS filter to be used. * @returns {string} The sanitized attribute value. * @memberof Sanitizer */ Sanitizer.prototype.sanitizeHTMLAttribute = function (tag, attribute, value, cssFilter) { // use the custom safeAttrValue function if provided if (typeof this.xssFilterOptions.safeAttrValue === "function") { return this.xssFilterOptions.safeAttrValue(tag, attribute, value, // @ts-expect-error safeAttrValue does handle undefined cssFilter cssFilter); } // otherwise use the default // @ts-ignore safeAttrValue does handle undefined cssFilter return xss__namespace.safeAttrValue(tag, attribute, value, cssFilter); }; /** * Checks if a value only contains valid HTML. * * @param {any} value The value to validate. * @returns {boolean} * @memberof Sanitizer */ Sanitizer.prototype.validate = function (value, options) { if (options === void 0) { options = {}; } var sanitized = this.sanitize(value, options); return { isValid: value === sanitized, sanitized: sanitized, }; }; /** * Encodes the following characters, `& < > \" ' /` to their hexadecimal HTML entity code. * Example: "·" => "8middot;" * * @param {string} value The value to encode. * @returns {string} The encoded string value. * @memberof Sanitizer */ Sanitizer.prototype.encodeHTML = function (value) { var _this = this; return String(value).replace(/[&<>"'\/]/g, function (s) { return _this._entityMap[s]; }); }; /** * Encodes all non-alphanumeric ASCII characters to their hexadecimal HTML entity codes. * Example: "alert(document.cookie)" => "alert(document.cookie)" * * @param {string} value The value to encode. * @returns {string} The encoded string value. * @memberof Sanitizer */ Sanitizer.prototype.encodeAttrValue = function (value) { var alphanumericRE = /^[a-zA-Z0-9]$/; return String(value).replace(/[\x00-\xFF]/g, function (c, idx) { return !alphanumericRE.test(c) ? "&#x".concat(Number(value.charCodeAt(idx)).toString(16), ";") : c; }); }; /** * Extends an object of arrays by by concatenating arrays of the same object * keys. If the if the previous key's value is not an array, the next key's * value will replace the previous key. This method is used for extending the * whiteList in the XSS filter options. * * @private * @param {Array<{}>} objects An array of objects. * @returns {{}} The extended object. * @memberof Sanitizer */ Sanitizer.prototype._extendObjectOfArrays = function (objects) { var finalObj = {}; objects.forEach(function (obj) { Object.keys(obj).forEach(function (key) { if (Array.isArray(obj[key]) && Array.isArray(finalObj[key])) { finalObj[key] = finalObj[key].concat(obj[key]); } else { finalObj[key] = obj[key]; } }); }); return finalObj; }; /** * Iterate over a plain object or array to deeply sanitize each value. * * @private * @param {object} obj The object to iterate over. * @returns {(object | null)} The sanitized object. * @memberof Sanitizer */ Sanitizer.prototype._iterateOverObject = function (obj, options) { var _this = this; if (options === void 0) { options = {}; } try { var hasChanged_1 = false; var changedObj = void 0; if (Array.isArray(obj)) { changedObj = obj.reduce(function (prev, value) { var validation = _this.validate(value, options); if (validation.isValid) { return prev.concat([value]); } else { hasChanged_1 = true; return prev.concat([validation.sanitized]); } }, []); } else if (!isPlainObject(obj)) { if (options.allowUndefined && typeof obj === "undefined") { return; } return null; } else { var keys = Object.keys(obj); changedObj = keys.reduce(function (prev, key) { var value = obj[key]; var validation = _this.validate(value, options); if (validation.isValid) { prev[key] = value; } else { hasChanged_1 = true; prev[key] = validation.sanitized; } return prev; }, {}); } if (hasChanged_1) { return changedObj; } return obj; } catch (err) { return null; } }; /** * Trim whitespace from the start and ends of a string. * @param {string} val The string to trim. * @returns {string} The trimmed string. */ Sanitizer.prototype._trim = function (val) { // @ts-ignore This is used by Jest, // but TypeScript errors since it assumes `trim` is always available. return String.prototype.trim ? val.trim() : val.replace(/(^\s*)|(\s*$)/g, ""); }; return Sanitizer; }()); exports.Sanitizer = Sanitizer; exports["default"] = Sanitizer;