url-sanitizer 0.7.4 → 0.7.5

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
@@ -35,7 +35,7 @@ import urlSanitizer, {
35
35
  ## sanitizeURL(url, opt)
36
36
 
37
37
  Sanitize the given URL.
38
- * `data` and `file` schemes must be explicitly allowed.
38
+ * `blob`, `data` and `file` schemes must be explicitly allowed.
39
39
 
40
40
  ### Parameters
41
41
 
@@ -61,7 +61,7 @@ console.log(decodeURIComponent(res1));
61
61
  const res2 = await sanitizeURL('data:text/html,<div><script>alert(1);</script></div><p onclick="alert(2)"></p>', {
62
62
  allow: ['data']
63
63
  })
64
- // -> 'data:text/html,%3Cdiv%3E%3C%2Fdiv%3E%3Cp%3E%3C%2Fp%3E'
64
+ // -> 'data:text/html,%3Cdiv%3E%3C/div%3E%3Cp%3E%3C/p%3E'
65
65
 
66
66
  console.log(decodeURIComponent(res2));
67
67
  // => 'data:text/html,<div></div><p></p>'
@@ -71,14 +71,14 @@ const base64data3 = btoa('<div><script>alert(1);</script></div>');
71
71
  const res3 = await sanitizeURL(`data:text/html;base64,${base64data3}`, {
72
72
  allow: ['data']
73
73
  })
74
- // => 'data:text/html,%3Cdiv%3E%3C%2Fdiv%3E'
74
+ // => 'data:text/html,%3Cdiv%3E%3C/div%3E'
75
75
 
76
76
  console.log(decodeURIComponent(res3));
77
77
  // => 'data:text/html,<div></div>'
78
78
 
79
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%3Cimg%3E%3C%2Fdiv%3E'
81
+ // => 'data:text/html,%3Cdiv%3E%3Cimg%3E%3C/div%3E'
82
82
 
83
83
  console.log(decodeURIComponent(res4));
84
84
  // => 'data:text/html,<div><img></div>'
@@ -152,7 +152,7 @@ Type: [object][3]
152
152
  * `valid` **[boolean][2]** Is valid URI.
153
153
  * `data` **[object][3]** Parsed result of data URL, `null`able.
154
154
  * `data.mime` **[string][1]** MIME type.
155
- * `data.base64` **[boolean][2]** `true` if base64 encoded.
155
+ * `data.base64` **[boolean][2]** Is base64 encoded.
156
156
  * `data.data` **[string][1]** Data part of the data URL.
157
157
  * `href` **[string][1]** Sanitized URL input.
158
158
  * `origin` **[string][1]** Scheme, domain and port of the sanitized URL.
@@ -196,12 +196,12 @@ const res3 = await parseURL('data:image/svg+xml;base64,PHN2Zz48ZyBvbmNsaWNrPSJhb
196
196
  data: {
197
197
  mime: 'image/svg+xml',
198
198
  base64: false,
199
- data: '%3Csvg%3E%3Cg%3E%3C%2Fg%3E%3C%2Fsvg%3E'
199
+ data: '%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E'
200
200
  },
201
- href: 'data:image/svg+xml,%3Csvg%3E%3Cg%3E%3C%2Fg%3E%3C%2Fsvg%3E',
201
+ href: 'data:image/svg+xml,%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E',
202
202
  origin: 'null',
203
203
  protocol: 'data:',
204
- pathname: 'image/svg+xml,%3Csvg%3E%3Cg%3E%3C%2Fg%3E%3C%2Fsvg%3E',
204
+ pathname: 'image/svg+xml,%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E',
205
205
  ...
206
206
  } */
207
207
 
@@ -231,7 +231,7 @@ Synchronous version of the [parseURL()](#parseURL).
231
231
 
232
232
  ## isURI(uri)
233
233
 
234
- Verify if the given URI is valid.
234
+ Verify if the given URI is valid and registered.
235
235
 
236
236
  ### Parameters
237
237
 
@@ -239,6 +239,7 @@ Verify if the given URI is valid.
239
239
 
240
240
  Returns **[Promise][5]<[boolean][2]>** Result.
241
241
  * Always `true` for `web+*` and `ext+*` schemes, except `web+javascript`, `web+vbscript`, `ext+javascript`, `ext+vbscript`.
242
+ * `false` for `javascript` and `vbscript` schemes.
242
243
 
243
244
  ```javascript
244
245
  const res1 = await isURI('https://example.com/foo');
@@ -1595,12 +1595,20 @@ var uri_schemes_default = [
1595
1595
  // bundle/mjs/uri-util.js
1596
1596
  var HEX = 16;
1597
1597
  var REG_BASE64 = /^[\da-z+/\-_=]+$/i;
1598
- var REG_DATA_URL = /data:[^,]*,[^"]+/g;
1598
+ var REG_DATA_URL = /data:[^,]*,/;
1599
1599
  var REG_DATA_URL_BASE64 = /data:[^,]*;?base64,[\da-z+/\-_=]+/i;
1600
+ var REG_DATA_URL_G = /data:[^,]*,[^"]+/g;
1601
+ var REG_END_COLON = /:$/;
1602
+ var REG_END_NUM = /(?:#|%23)$/;
1603
+ var REG_END_QUEST = /(?<!(?:#|%23).*)(?:\?|%3F)$/;
1600
1604
  var REG_HTML_SP = /[<>"'\s]/g;
1601
- var REG_HTML_SP_URL_ENC = /%(?:2(?:2|7)|3(?:C|E))/g;
1602
- var REG_HTML_SP_URL_ENC_SHORT = /%(?:2(?:2|7)|3(?:C|E))+?/;
1605
+ var REG_HTML_URL_ENC = /%(?:2(?:2|7)|3(?:C|E))/g;
1606
+ var REG_HTML_URL_ENC_SHORT = /%(?:2(?:2|7)|3(?:C|E))+?/;
1603
1607
  var REG_MIME_DOM = /^(?:text\/(?:ht|x)ml|application\/(?:xhtml\+)?xml|image\/svg\+xml)/;
1608
+ var REG_NUM_DECI = /^\d+/;
1609
+ var REG_NUM_HEAD = /#x?$/;
1610
+ var REG_NUM_HEAD_ASCII = /^#(?:x(?:00)?[2-7]|\d)/;
1611
+ var REG_NUM_HEX = /^x[\dA-F]+/i;
1604
1612
  var REG_NUM_REF = /&#(x(?:00)?[\dA-F]{2}|0?\d{1,3});?/ig;
1605
1613
  var REG_SCHEME = /^[a-z][\da-z+\-.]*$/;
1606
1614
  var REG_SCHEME_CUSTOM = /^(?:ext|web)\+[a-z]+$/;
@@ -1671,9 +1679,9 @@ var parseURLEncodedNumCharRef = (str, nest = 0) => {
1671
1679
  for (const item of items) {
1672
1680
  const [numCharRef, value] = item;
1673
1681
  let num;
1674
- if (/^x[\dA-F]+/i.test(value)) {
1682
+ if (REG_NUM_HEX.test(value)) {
1675
1683
  num = parseInt(`0${value}`, HEX);
1676
- } else if (/^[\d]+/.test(value)) {
1684
+ } else if (REG_NUM_DECI.test(value)) {
1677
1685
  num = parseInt(value);
1678
1686
  }
1679
1687
  if (Number.isInteger(num)) {
@@ -1684,7 +1692,7 @@ var parseURLEncodedNumCharRef = (str, nest = 0) => {
1684
1692
  ];
1685
1693
  if (textCharCodes.has(num)) {
1686
1694
  res = `${preNum}${String.fromCharCode(num)}${postNum}`;
1687
- if (/#x?$/.test(preNum) || /^#(?:x(?:00)?[2-7]|\d)/.test(postNum)) {
1695
+ if (REG_NUM_HEAD.test(preNum) || REG_NUM_HEAD_ASCII.test(postNum)) {
1688
1696
  res = parseURLEncodedNumCharRef(res, ++nest);
1689
1697
  }
1690
1698
  } else if (num < HEX * HEX) {
@@ -1752,7 +1760,7 @@ var URISchemes = class {
1752
1760
  /**
1753
1761
  * verify URI
1754
1762
  *
1755
- * @param {string} uri - URI input
1763
+ * @param {string} uri - URI
1756
1764
  * @returns {boolean} - result
1757
1765
  */
1758
1766
  verify(uri) {
@@ -1760,7 +1768,7 @@ var URISchemes = class {
1760
1768
  if (isString(uri)) {
1761
1769
  try {
1762
1770
  const { protocol } = new URL(uri);
1763
- const scheme = protocol.replace(/:$/, "");
1771
+ const scheme = protocol.replace(REG_END_COLON, "");
1764
1772
  const schemeParts = scheme.split("+");
1765
1773
  res = !REG_SCRIPT.test(scheme) && REG_SCHEME_CUSTOM.test(scheme) || schemeParts.every((s) => this.#schemes.has(s));
1766
1774
  } catch (e) {
@@ -1783,18 +1791,18 @@ var URLSanitizer = class extends URISchemes {
1783
1791
  this.#recurse = /* @__PURE__ */ new Set();
1784
1792
  }
1785
1793
  /**
1786
- * purify URL encoded DOM
1794
+ * replace matched data URLs
1787
1795
  *
1788
- * @param {string} dom - DOM input
1789
- * @returns {string} - purified DOM
1796
+ * @param {string} data - data URL
1797
+ * @returns {string} - replaced data URL
1790
1798
  */
1791
- purify(dom) {
1792
- if (!isString(dom)) {
1793
- throw new TypeError(`Expected String but got ${getType(dom)}.`);
1799
+ replace(data) {
1800
+ if (!isString(data)) {
1801
+ throw new TypeError(`Expected String but got ${getType(data)}.`);
1794
1802
  }
1795
- let purifiedDom = import_dompurify.default.sanitize(decodeURIComponent(dom));
1796
- if (purifiedDom) {
1797
- const matchedDataUrls = purifiedDom.matchAll(REG_DATA_URL);
1803
+ let replacedData = data;
1804
+ if (REG_DATA_URL.test(replacedData)) {
1805
+ const matchedDataUrls = replacedData.matchAll(REG_DATA_URL_G);
1798
1806
  const items = [...matchedDataUrls].reverse();
1799
1807
  for (const item of items) {
1800
1808
  let [dataUrl] = item;
@@ -1808,24 +1816,41 @@ var URLSanitizer = class extends URISchemes {
1808
1816
  });
1809
1817
  const { index } = item;
1810
1818
  const [preDataUrl, postDataUrl] = [
1811
- purifiedDom.substring(0, index),
1812
- purifiedDom.substring(index + dataUrl.length)
1819
+ replacedData.substring(0, index),
1820
+ replacedData.substring(index + dataUrl.length)
1813
1821
  ];
1814
1822
  if (parsedDataUrl) {
1815
- purifiedDom = `${preDataUrl}${parsedDataUrl}${postDataUrl}`;
1823
+ replacedData = `${preDataUrl}${parsedDataUrl}${postDataUrl}`;
1816
1824
  } else {
1817
- purifiedDom = `${preDataUrl}${postDataUrl}`;
1825
+ replacedData = `${preDataUrl}${postDataUrl}`;
1818
1826
  }
1819
1827
  }
1820
1828
  }
1821
- return encodeURIComponent(purifiedDom);
1829
+ return replacedData;
1830
+ }
1831
+ /**
1832
+ * purify URL encoded DOM
1833
+ *
1834
+ * @param {string} dom - DOM string
1835
+ * @returns {string} - purified DOM string
1836
+ */
1837
+ purify(dom) {
1838
+ if (!isString(dom)) {
1839
+ throw new TypeError(`Expected String but got ${getType(dom)}.`);
1840
+ }
1841
+ let purifiedDom = import_dompurify.default.sanitize(decodeURIComponent(dom));
1842
+ if (purifiedDom && REG_DATA_URL.test(purifiedDom)) {
1843
+ purifiedDom = this.replace(purifiedDom);
1844
+ }
1845
+ purifiedDom = purifiedDom.replace(REG_END_NUM, "").replace(REG_END_QUEST, "");
1846
+ return encodeURI(purifiedDom);
1822
1847
  }
1823
1848
  /**
1824
1849
  * sanitize URL
1825
- * NOTE: `data` and/or `file` schemes must be explicitly allowed
1826
- * `javascript` and/or `vbscript` schemes can not be allowed
1850
+ * NOTE: `blob`, `data` and `file` schemes must be explicitly allowed
1851
+ * `javascript` and `vbscript` schemes can not be allowed
1827
1852
  *
1828
- * @param {string} url - URL input
1853
+ * @param {string} url - URL
1829
1854
  * @param {object} opt - options
1830
1855
  * @param {Array.<string>} opt.allow - array of allowed schemes
1831
1856
  * @param {Array.<string>} opt.deny - array of denied schemes
@@ -1840,6 +1865,7 @@ var URLSanitizer = class extends URISchemes {
1840
1865
  }
1841
1866
  const { allow, deny, only, remove } = opt ?? {};
1842
1867
  const schemeMap = /* @__PURE__ */ new Map([
1868
+ ["blob", false],
1843
1869
  ["data", false],
1844
1870
  ["file", false],
1845
1871
  ["javascrpt", false],
@@ -1910,7 +1936,7 @@ var URLSanitizer = class extends URISchemes {
1910
1936
  let sanitizedUrl;
1911
1937
  if (super.verify(url)) {
1912
1938
  const { hash, href, pathname, protocol, search } = new URL(url);
1913
- const scheme = protocol.replace(/:$/, "");
1939
+ const scheme = protocol.replace(REG_END_COLON, "");
1914
1940
  const schemeParts = scheme.split("+");
1915
1941
  let bool;
1916
1942
  if (restrictScheme) {
@@ -1924,9 +1950,10 @@ var URLSanitizer = class extends URISchemes {
1924
1950
  }
1925
1951
  }
1926
1952
  if (bool) {
1927
- let escapeHtml;
1953
+ const isDataUrl = schemeParts.includes("data");
1954
+ let finalize;
1928
1955
  let urlToSanitize = href;
1929
- if (schemeParts.includes("data")) {
1956
+ if (isDataUrl) {
1930
1957
  const [head, ...body] = pathname.split(",");
1931
1958
  const data = `${body.join(",")}${search}${hash}`;
1932
1959
  const mediaType = head.split(";");
@@ -1934,58 +1961,32 @@ var URLSanitizer = class extends URISchemes {
1934
1961
  let parsedData = data;
1935
1962
  if (isBase64) {
1936
1963
  parsedData = parseBase64(data);
1937
- } else {
1938
- try {
1939
- const decodedData = parseURLEncodedNumCharRef(parsedData);
1940
- const { protocol: dataScheme } = new URL(decodedData.trim());
1941
- const dataSchemeParts = dataScheme.replace(/:$/, "").split("+");
1942
- if (dataSchemeParts.some((s) => REG_SCRIPT.test(s))) {
1943
- urlToSanitize = "";
1944
- }
1945
- } catch (e) {
1964
+ }
1965
+ try {
1966
+ const decodedData = parseURLEncodedNumCharRef(parsedData);
1967
+ const { protocol: dataScheme } = new URL(decodedData.trim());
1968
+ const dataSchemeParts = dataScheme.replace(REG_END_COLON, "").split("+");
1969
+ if (dataSchemeParts.some((s) => REG_SCRIPT.test(s))) {
1970
+ urlToSanitize = "";
1946
1971
  }
1972
+ } catch (e) {
1947
1973
  }
1948
- const containsDataUrl = /data:[^,]*,/.test(parsedData);
1974
+ const containsDataUrl = REG_DATA_URL.test(parsedData);
1949
1975
  if (parsedData !== data || containsDataUrl) {
1950
1976
  if (containsDataUrl) {
1951
- const matchedDataUrls = parsedData.matchAll(REG_DATA_URL);
1952
- const items = [...matchedDataUrls].reverse();
1953
- for (const item of items) {
1954
- let [dataUrl] = item;
1955
- if (REG_DATA_URL_BASE64.test(dataUrl)) {
1956
- [dataUrl] = REG_DATA_URL_BASE64.exec(dataUrl);
1957
- }
1958
- this.#nest++;
1959
- this.#recurse.add(dataUrl);
1960
- const parsedDataUrl = this.sanitize(dataUrl, {
1961
- allow: ["data"]
1962
- });
1963
- if (parsedDataUrl) {
1964
- const { index } = item;
1965
- const [preDataUrl, postDataUrl] = [
1966
- parsedData.substring(0, index),
1967
- parsedData.substring(index + dataUrl.length)
1968
- ];
1969
- parsedData = `${preDataUrl}${parsedDataUrl}${postDataUrl}`;
1970
- }
1971
- }
1972
- if (this.#recurse.has(url)) {
1973
- this.#recurse.delete(url);
1974
- } else {
1975
- escapeHtml = true;
1976
- }
1977
+ parsedData = this.replace(parsedData);
1977
1978
  } else if (this.#recurse.has(url)) {
1978
1979
  this.#recurse.delete(url);
1979
1980
  } else {
1980
- escapeHtml = true;
1981
+ finalize = true;
1981
1982
  }
1982
1983
  } else if (this.#recurse.has(url)) {
1983
1984
  this.#recurse.delete(url);
1984
1985
  } else {
1985
- escapeHtml = true;
1986
+ finalize = true;
1986
1987
  }
1987
- if (REG_MIME_DOM.test(head)) {
1988
- parsedData = this.purify(parsedData).replace(/%23$/, "").replace(/%3F$/, "");
1988
+ if (!head || REG_MIME_DOM.test(head)) {
1989
+ parsedData = this.purify(parsedData);
1989
1990
  }
1990
1991
  if (urlToSanitize && parsedData) {
1991
1992
  if (isBase64 && parsedData !== data) {
@@ -1996,18 +1997,18 @@ var URLSanitizer = class extends URISchemes {
1996
1997
  urlToSanitize = "";
1997
1998
  }
1998
1999
  } else {
1999
- escapeHtml = true;
2000
+ finalize = true;
2000
2001
  }
2001
- if (!schemeParts.includes("data") && remove && REG_HTML_SP_URL_ENC_SHORT.test(urlToSanitize)) {
2002
- const item = REG_HTML_SP_URL_ENC_SHORT.exec(urlToSanitize);
2002
+ if (!isDataUrl && remove && REG_HTML_URL_ENC_SHORT.test(urlToSanitize)) {
2003
+ const item = REG_HTML_URL_ENC_SHORT.exec(urlToSanitize);
2003
2004
  const { index } = item;
2004
2005
  urlToSanitize = urlToSanitize.substring(0, index);
2005
2006
  }
2006
2007
  if (urlToSanitize) {
2007
2008
  sanitizedUrl = urlToSanitize.replace(REG_HTML_SP, getURLEncodedString).replace(REG_URL_ENC_AMP, escapeURLEncodedHTMLChars);
2008
- if (escapeHtml) {
2009
- if (!schemeParts.includes("data")) {
2010
- sanitizedUrl = sanitizedUrl.replace(REG_HTML_SP_URL_ENC, escapeURLEncodedHTMLChars);
2009
+ if (finalize) {
2010
+ if (!isDataUrl) {
2011
+ sanitizedUrl = sanitizedUrl.replace(REG_HTML_URL_ENC, escapeURLEncodedHTMLChars);
2011
2012
  }
2012
2013
  this.#nest = 0;
2013
2014
  }
@@ -2027,7 +2028,7 @@ var URLSanitizer = class extends URISchemes {
2027
2028
  * @property {boolean} valid - is valid URI
2028
2029
  * @property {object} data - parsed result of data URL, `null`able
2029
2030
  * @property {string} data.mime - MIME type
2030
- * @property {boolean} data.base64 - `true` if base64 encoded
2031
+ * @property {boolean} data.base64 - is base64 encoded
2031
2032
  * @property {string} data.data - data part of the data URL
2032
2033
  * @property {string} href - sanitized URL input
2033
2034
  * @property {string} origin - scheme, domain and port of the sanitized URL
@@ -2039,13 +2040,12 @@ var URLSanitizer = class extends URISchemes {
2039
2040
  * @property {string} port - port number of the sanitized URL
2040
2041
  * @property {string} pathname - path of the sanitized URL
2041
2042
  * @property {string} search - query string of the sanitized URL
2042
- * @property {object} searchParams - URLSearchParams object
2043
2043
  * @property {string} hash - fragment identifier of the sanitized URL
2044
2044
  */
2045
2045
  /**
2046
2046
  * parse sanitized URL
2047
2047
  *
2048
- * @param {string} url - URL input
2048
+ * @param {string} url - URL
2049
2049
  * @param {object} opt - options
2050
2050
  * @returns {ParsedURL} - result with enhanced props based on URL API
2051
2051
  */
@@ -2054,7 +2054,7 @@ var URLSanitizer = class extends URISchemes {
2054
2054
  throw new TypeError(`Expected String but got ${getType(url)}.`);
2055
2055
  }
2056
2056
  const sanitizedUrl = this.sanitize(url, opt ?? {
2057
- allow: ["data", "file"]
2057
+ allow: ["blob", "data", "file"]
2058
2058
  });
2059
2059
  const parsedUrl = /* @__PURE__ */ new Map([
2060
2060
  ["input", url]
@@ -2062,9 +2062,10 @@ var URLSanitizer = class extends URISchemes {
2062
2062
  if (sanitizedUrl) {
2063
2063
  const urlObj = new URL(sanitizedUrl);
2064
2064
  const { pathname, protocol } = urlObj;
2065
- const schemeParts = protocol.replace(/:$/, "").split("+");
2065
+ const schemeParts = protocol.replace(REG_END_COLON, "").split("+");
2066
+ const isDataUrl = schemeParts.includes("data");
2066
2067
  parsedUrl.set("valid", true);
2067
- if (schemeParts.includes("data")) {
2068
+ if (isDataUrl) {
2068
2069
  const dataUrl = /* @__PURE__ */ new Map();
2069
2070
  const [head, ...body] = pathname.split(",");
2070
2071
  const data = `${body.join(",")}`;
@@ -2103,19 +2104,11 @@ var parseURL = async (url) => {
2103
2104
  const res = await parseURLSync(url);
2104
2105
  return res;
2105
2106
  };
2106
- var sanitizeURLSync = (url, opt) => {
2107
- const parsedUrl = urlSanitizer.parse(url, opt ?? {
2108
- allow: [],
2109
- deny: [],
2110
- only: []
2111
- });
2112
- let res;
2113
- if (parsedUrl) {
2114
- const { href } = parsedUrl;
2115
- res = href;
2116
- }
2117
- return res ?? null;
2118
- };
2107
+ var sanitizeURLSync = (url, opt) => urlSanitizer.sanitize(url, opt ?? {
2108
+ allow: [],
2109
+ deny: [],
2110
+ only: []
2111
+ });
2119
2112
  var sanitizeURL = async (url, opt) => {
2120
2113
  const res = await sanitizeURLSync(url, opt);
2121
2114
  return res;