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 +7 -7
- package/dist/url-sanitizer.js +63 -71
- package/dist/url-sanitizer.js.map +2 -2
- package/dist/url-sanitizer.min.js +3 -3
- package/dist/url-sanitizer.min.js.map +3 -3
- package/package.json +2 -2
- package/src/mjs/uri-util.js +66 -64
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.
|
|
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><
|
|
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?<script>alert(1)</script>'
|
|
114
114
|
|
|
115
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
122
|
+
remove: true
|
|
123
123
|
});
|
|
124
124
|
// => 'https://example.com/?'
|
|
125
125
|
```
|
package/dist/url-sanitizer.js
CHANGED
|
@@ -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.
|
|
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,
|
|
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 (
|
|
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") &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
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 {
|