url-sanitizer 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,7 +45,7 @@ Sanitize the given URL.
45
45
  * `opt.deny` **[Array][4]<[string][1]>** Array of denied schemes, e.g. `['web+foo']`.
46
46
  * `opt.only` **[Array][4]<[string][1]>** Array of specific schemes to allow, e.g. `['git', 'https']`.
47
47
  `only` takes precedence over `allow` and `deny`.
48
- * `opt.truncate` **[boolean][2]** Truncate strings matching tags and quotes.
48
+ * `opt.remove` **[boolean][2]** Remove tag or quote and the rest following it.
49
49
 
50
50
  Returns **[Promise][5]<[string][1]?>** Sanitized URL, `null`able.
51
51
 
@@ -76,12 +76,12 @@ const res3 = await sanitizeURL(`data:text/html;base64,${base64data3}`, {
76
76
  console.log(decodeURIComponent(res3));
77
77
  // => 'data:text/html,<div></div>'
78
78
 
79
- const base64data4 = btoa('<div><iframe src="javascript:alert(1)"></iframe></div>');
79
+ const base64data4 = btoa('<div><img src="javascript:alert(1)"></div>');
80
80
  const res4 = await sanitizeURL(`data:text/html;base64,${base64data4}`);
81
- // => 'data:text/html,%3Cdiv%3E%3C%2Fdiv%3E'
81
+ // => 'data:text/html,%3Cdiv%3E%3Cimg%3E%3C%2Fdiv%3E'
82
82
 
83
83
  console.log(decodeURIComponent(res4));
84
- // => 'data:text/html,<div></div>'
84
+ // => 'data:text/html,<div><img></div>'
85
85
 
86
86
  // Deny if the scheme matches the `deny` list
87
87
  const res5 = await sanitizeURL('web+foo://example.com', {
@@ -112,14 +112,14 @@ const res8 = await sanitizeURL('git+https://example.com/foo.git?<script>alert(1)
112
112
  console.log(decodeURIComponent(res8));
113
113
  // => 'git+https://example.com/foo.git?&lt;script&gt;alert(1)&lt;/script&gt;'
114
114
 
115
- // Truncate the tag or quote and the rest of the URL following it.
115
+ // Remove the tag or quote and the rest of the URL following it.
116
116
  const res9 = await sanitizeURL('https://example.com/" onclick="alert(1)"', {
117
- truncate: true
117
+ remove: true
118
118
  });
119
119
  // => 'https://example.com/'
120
120
 
121
121
  const res10 = await sanitizeURL('https://example.com/?<script>alert(1)</script>', {
122
- truncate: true
122
+ remove: true
123
123
  });
124
124
  // => 'https://example.com/?'
125
125
  ```
@@ -1607,7 +1607,6 @@ var REG_SCHEME_CUSTOM = /^(?:ext|web)\+[a-z]+$/;
1607
1607
  var REG_SCRIPT = /(?:java|vb)script/;
1608
1608
  var REG_URL_ENC = /^%[\dA-F]{2}$/i;
1609
1609
  var REG_URL_ENC_AMP = /%26/g;
1610
- var REG_URL_ENC_HTML_SP = /%26(?:(?:l|g|quo)t|%2339);?/g;
1611
1610
  var getURLEncodedString = (str) => {
1612
1611
  if (!isString(str)) {
1613
1612
  throw new TypeError(`Expected String but got ${getType(str)}.`);
@@ -1639,21 +1638,6 @@ var escapeURLEncodedHTMLChars = (ch) => {
1639
1638
  }
1640
1639
  return escapedChar;
1641
1640
  };
1642
- var unescapeURLEncodedHTMLChars = (ch) => {
1643
- let unescapedChar;
1644
- if (/%26lt;?/.test(ch)) {
1645
- unescapedChar = "<";
1646
- } else if (/%26gt;?/.test(ch)) {
1647
- unescapedChar = ">";
1648
- } else if (/%26quot;?/.test(ch)) {
1649
- unescapedChar = '"';
1650
- } else if (/%26%2339;?/.test(ch)) {
1651
- unescapedChar = "'";
1652
- } else {
1653
- unescapedChar = ch;
1654
- }
1655
- return unescapedChar;
1656
- };
1657
1641
  var parseBase64 = (data) => {
1658
1642
  if (!isString(data)) {
1659
1643
  throw new TypeError(`Expected String but got ${getType(data)}.`);
@@ -1711,12 +1695,6 @@ var parseURLEncodedNumCharRef = (str, nest = 0) => {
1711
1695
  }
1712
1696
  return res;
1713
1697
  };
1714
- var purifyURLEncodedDOM = (dom) => {
1715
- if (!isString(dom)) {
1716
- throw new TypeError(`Expected String but got ${getType(dom)}.`);
1717
- }
1718
- return encodeURIComponent(import_dompurify.default.sanitize(decodeURIComponent(dom)));
1719
- };
1720
1698
  var URISchemes = class {
1721
1699
  /* private fields */
1722
1700
  #schemes;
@@ -1804,6 +1782,44 @@ var URLSanitizer = class extends URISchemes {
1804
1782
  this.#nest = 0;
1805
1783
  this.#recurse = /* @__PURE__ */ new Set();
1806
1784
  }
1785
+ /**
1786
+ * purify URL encoded DOM
1787
+ *
1788
+ * @param {string} dom - DOM input
1789
+ * @returns {string} - purified DOM
1790
+ */
1791
+ purify(dom) {
1792
+ if (!isString(dom)) {
1793
+ throw new TypeError(`Expected String but got ${getType(dom)}.`);
1794
+ }
1795
+ let purifiedDom = import_dompurify.default.sanitize(decodeURIComponent(dom));
1796
+ if (purifiedDom) {
1797
+ const matchedDataUrls = purifiedDom.matchAll(REG_DATA_URL);
1798
+ const items = [...matchedDataUrls].reverse();
1799
+ for (const item of items) {
1800
+ let [dataUrl] = item;
1801
+ if (REG_DATA_URL_BASE64.test(dataUrl)) {
1802
+ [dataUrl] = REG_DATA_URL_BASE64.exec(dataUrl);
1803
+ }
1804
+ this.#nest++;
1805
+ this.#recurse.add(dataUrl);
1806
+ const parsedDataUrl = this.sanitize(dataUrl, {
1807
+ allow: ["data"]
1808
+ });
1809
+ const { index } = item;
1810
+ const [preDataUrl, postDataUrl] = [
1811
+ purifiedDom.substring(0, index),
1812
+ purifiedDom.substring(index + dataUrl.length)
1813
+ ];
1814
+ if (parsedDataUrl) {
1815
+ purifiedDom = `${preDataUrl}${parsedDataUrl}${postDataUrl}`;
1816
+ } else {
1817
+ purifiedDom = `${preDataUrl}${postDataUrl}`;
1818
+ }
1819
+ }
1820
+ }
1821
+ return encodeURIComponent(purifiedDom);
1822
+ }
1807
1823
  /**
1808
1824
  * sanitize URL
1809
1825
  * NOTE: `data` and/or `file` schemes must be explicitly allowed
@@ -1814,7 +1830,7 @@ var URLSanitizer = class extends URISchemes {
1814
1830
  * @param {Array.<string>} opt.allow - array of allowed schemes
1815
1831
  * @param {Array.<string>} opt.deny - array of denied schemes
1816
1832
  * @param {Array.<string>} opt.only - array of specific schemes to allow
1817
- * @param {boolean} opt.truncate - truncate tag and/or quote and the rest
1833
+ * @param {boolean} opt.remove - remove tag and/or quote and the rest
1818
1834
  * @returns {?string} - sanitized URL
1819
1835
  */
1820
1836
  sanitize(url, opt = { allow: [], deny: [], only: [] }) {
@@ -1822,7 +1838,7 @@ var URLSanitizer = class extends URISchemes {
1822
1838
  this.#nest = 0;
1823
1839
  throw new Error("Data URLs nested too deeply.");
1824
1840
  }
1825
- const { allow, deny, only, truncate } = opt ?? {};
1841
+ const { allow, deny, only, remove } = opt ?? {};
1826
1842
  const schemeMap = /* @__PURE__ */ new Map([
1827
1843
  ["data", false],
1828
1844
  ["file", false],
@@ -1914,9 +1930,9 @@ var URLSanitizer = class extends URISchemes {
1914
1930
  const [head, ...body] = pathname.split(",");
1915
1931
  const data = `${body.join(",")}${search}${hash}`;
1916
1932
  const mediaType = head.split(";");
1933
+ const isBase64 = mediaType[mediaType.length - 1] === "base64";
1917
1934
  let parsedData = data;
1918
- if (mediaType[mediaType.length - 1] === "base64") {
1919
- mediaType.pop();
1935
+ if (isBase64) {
1920
1936
  parsedData = parseBase64(data);
1921
1937
  } else {
1922
1938
  try {
@@ -1963,16 +1979,26 @@ var URLSanitizer = class extends URISchemes {
1963
1979
  } else {
1964
1980
  escapeHtml = true;
1965
1981
  }
1966
- urlToSanitize = `${scheme}:${mediaType.join(";")},${parsedData}`;
1967
1982
  } else if (this.#recurse.has(url)) {
1968
1983
  this.#recurse.delete(url);
1969
1984
  } else {
1970
1985
  escapeHtml = true;
1971
1986
  }
1987
+ if (REG_MIME_DOM.test(head)) {
1988
+ parsedData = this.purify(parsedData).replace(/%23$/, "").replace(/%3F$/, "");
1989
+ }
1990
+ if (urlToSanitize && parsedData) {
1991
+ if (isBase64 && parsedData !== data) {
1992
+ mediaType.pop();
1993
+ }
1994
+ urlToSanitize = `${scheme}:${mediaType.join(";")},${parsedData}`;
1995
+ } else {
1996
+ urlToSanitize = "";
1997
+ }
1972
1998
  } else {
1973
1999
  escapeHtml = true;
1974
2000
  }
1975
- if (!schemeParts.includes("data") && truncate && REG_HTML_SP_URL_ENC_SHORT.test(urlToSanitize)) {
2001
+ if (!schemeParts.includes("data") && remove && REG_HTML_SP_URL_ENC_SHORT.test(urlToSanitize)) {
1976
2002
  const item = REG_HTML_SP_URL_ENC_SHORT.exec(urlToSanitize);
1977
2003
  const { index } = item;
1978
2004
  urlToSanitize = urlToSanitize.substring(0, index);
@@ -1980,7 +2006,9 @@ var URLSanitizer = class extends URISchemes {
1980
2006
  if (urlToSanitize) {
1981
2007
  sanitizedUrl = urlToSanitize.replace(REG_HTML_SP, getURLEncodedString).replace(REG_URL_ENC_AMP, escapeURLEncodedHTMLChars);
1982
2008
  if (escapeHtml) {
1983
- sanitizedUrl = sanitizedUrl.replace(REG_HTML_SP_URL_ENC, escapeURLEncodedHTMLChars);
2009
+ if (!schemeParts.includes("data")) {
2010
+ sanitizedUrl = sanitizedUrl.replace(REG_HTML_SP_URL_ENC, escapeURLEncodedHTMLChars);
2011
+ }
1984
2012
  this.#nest = 0;
1985
2013
  }
1986
2014
  } else {
@@ -2047,51 +2075,15 @@ var URLSanitizer = class extends URISchemes {
2047
2075
  }
2048
2076
  dataUrl.set("mime", mediaType.join(";"));
2049
2077
  dataUrl.set("base64", isBase64);
2050
- let purifiedDom;
2051
- if (!isBase64 && REG_MIME_DOM.test(head)) {
2052
- let parsedData = data;
2053
- const matchedHtmlChars = parsedData.matchAll(REG_URL_ENC_HTML_SP);
2054
- const items = [...matchedHtmlChars].reverse();
2055
- for (const item of items) {
2056
- const [htmlChar] = item;
2057
- const { index } = item;
2058
- const [preHtmlChar, postHtmlChar] = [
2059
- parsedData.substring(0, index),
2060
- parsedData.substring(index + htmlChar.length)
2061
- ];
2062
- const unescapedHtmlChar = unescapeURLEncodedHTMLChars(htmlChar);
2063
- parsedData = `${preHtmlChar}${unescapedHtmlChar}${postHtmlChar}`;
2064
- }
2065
- purifiedDom = purifyURLEncodedDOM(parsedData);
2066
- dataUrl.set("data", purifiedDom);
2067
- } else {
2068
- dataUrl.set("data", data);
2069
- }
2078
+ dataUrl.set("data", data);
2070
2079
  parsedUrl.set("data", Object.fromEntries(dataUrl));
2071
- for (const key in urlObj) {
2072
- let value = urlObj[key];
2073
- if (isString(value)) {
2074
- if (isString(purifiedDom)) {
2075
- switch (key) {
2076
- case "href":
2077
- value = `${protocol}${mediaType.join(";")},${purifiedDom}`;
2078
- break;
2079
- case "pathname":
2080
- value = `${mediaType.join(";")},${purifiedDom}`;
2081
- break;
2082
- default:
2083
- }
2084
- }
2085
- parsedUrl.set(key, value);
2086
- }
2087
- }
2088
2080
  } else {
2089
2081
  parsedUrl.set("data", null);
2090
- for (const key in urlObj) {
2091
- const value = urlObj[key];
2092
- if (isString(value)) {
2093
- parsedUrl.set(key, value);
2094
- }
2082
+ }
2083
+ for (const key in urlObj) {
2084
+ const value = urlObj[key];
2085
+ if (isString(value)) {
2086
+ parsedUrl.set(key, value);
2095
2087
  }
2096
2088
  }
2097
2089
  } else {