url-sanitizer 0.7.0 → 0.7.2
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 +22 -9
- package/dist/url-sanitizer.js +42 -41
- 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 +7 -5
- package/src/mjs/uri-util.js +58 -59
package/README.md
CHANGED
|
@@ -45,6 +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
49
|
|
|
49
50
|
Returns **[Promise][5]<[string][1]?>** Sanitized URL, `null`able.
|
|
50
51
|
|
|
@@ -82,11 +83,13 @@ const res4 = await sanitizeURL(`data:text/html;base64,${base64data4}`);
|
|
|
82
83
|
console.log(decodeURIComponent(res4));
|
|
83
84
|
// => 'data:text/html,<div></div>'
|
|
84
85
|
|
|
86
|
+
// Deny if the scheme matches the `deny` list
|
|
85
87
|
const res5 = await sanitizeURL('web+foo://example.com', {
|
|
86
88
|
deny: ['web+foo']
|
|
87
89
|
});
|
|
88
90
|
// => null
|
|
89
91
|
|
|
92
|
+
// Allow only if the scheme matches the `only` list
|
|
90
93
|
const res6 = await sanitizeURL('http://example.com', {
|
|
91
94
|
only: ['data', 'git', 'https']
|
|
92
95
|
});
|
|
@@ -100,7 +103,7 @@ const res7 = await sanitizeURL('https://example.com/"onmouseover="alert(1)"', {
|
|
|
100
103
|
console.log(decodeURIComponent(res7));
|
|
101
104
|
// => 'https://example.com/"onmouseover="alert(1)"'
|
|
102
105
|
|
|
103
|
-
// `only`
|
|
106
|
+
// `only` also allows combination of the schemes in the list
|
|
104
107
|
const res8 = await sanitizeURL('git+https://example.com/foo.git?<script>alert(1)</script>', {
|
|
105
108
|
only: ['data', 'git', 'https']
|
|
106
109
|
});
|
|
@@ -108,6 +111,17 @@ const res8 = await sanitizeURL('git+https://example.com/foo.git?<script>alert(1)
|
|
|
108
111
|
|
|
109
112
|
console.log(decodeURIComponent(res8));
|
|
110
113
|
// => 'git+https://example.com/foo.git?<script>alert(1)</script>'
|
|
114
|
+
|
|
115
|
+
// Truncate the tag or quote and the rest of the URL following it.
|
|
116
|
+
const res9 = await sanitizeURL('https://example.com/" onclick="alert(1)"', {
|
|
117
|
+
truncate: true
|
|
118
|
+
});
|
|
119
|
+
// => 'https://example.com/'
|
|
120
|
+
|
|
121
|
+
const res10 = await sanitizeURL('https://example.com/?<script>alert(1)</script>', {
|
|
122
|
+
truncate: true
|
|
123
|
+
});
|
|
124
|
+
// => 'https://example.com/?'
|
|
111
125
|
```
|
|
112
126
|
|
|
113
127
|
|
|
@@ -128,7 +142,7 @@ Returns **[Promise][5]<[ParsedURL](#parsedurl)>** Result.
|
|
|
128
142
|
|
|
129
143
|
### ParsedURL
|
|
130
144
|
|
|
131
|
-
Object with
|
|
145
|
+
Object with additional properties based on [URL](https://developer.mozilla.org/ja/docs/Web/API/URL) API.
|
|
132
146
|
|
|
133
147
|
Type: [object][3]
|
|
134
148
|
|
|
@@ -150,17 +164,16 @@ Type: [object][3]
|
|
|
150
164
|
* `port` **[string][1]** Port number of the sanitized URL.
|
|
151
165
|
* `pathname` **[string][1]** Path of the sanitized URL.
|
|
152
166
|
* `search` **[string][1]** Query string of the sanitized URL.
|
|
153
|
-
* `searchParams` **[object][3]** [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) object.
|
|
154
167
|
* `hash` **[string][1]** Fragment identifier of the sanitized URL.
|
|
155
168
|
|
|
156
169
|
```javascript
|
|
157
170
|
const res1 = await parseURL('javascript:alert(1)');
|
|
158
171
|
/* => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
} */
|
|
172
|
+
input: 'javascript:alert(1)',
|
|
173
|
+
valid: false
|
|
174
|
+
} */
|
|
162
175
|
|
|
163
|
-
const res2 = await parseURL('https://example.com/?foo=bar#baz');
|
|
176
|
+
const res2 = await parseURL('https://www.example.com/?foo=bar#baz');
|
|
164
177
|
/* => {
|
|
165
178
|
input: 'https://www.example.com/?foo=bar#baz',
|
|
166
179
|
valid: true,
|
|
@@ -192,7 +205,7 @@ const res3 = await parseURL('data:image/svg+xml;base64,PHN2Zz48ZyBvbmNsaWNrPSJhb
|
|
|
192
205
|
...
|
|
193
206
|
} */
|
|
194
207
|
|
|
195
|
-
// base64 encoded
|
|
208
|
+
// base64 encoded PNG
|
|
196
209
|
const res4 = await parseURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==');
|
|
197
210
|
/* => {
|
|
198
211
|
input: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
|
|
@@ -218,7 +231,7 @@ Synchronous version of the [parseURL()](#parseURL).
|
|
|
218
231
|
|
|
219
232
|
## isURI(uri)
|
|
220
233
|
|
|
221
|
-
|
|
234
|
+
Verify if the given URI is valid.
|
|
222
235
|
|
|
223
236
|
### Parameters
|
|
224
237
|
|
package/dist/url-sanitizer.js
CHANGED
|
@@ -1597,9 +1597,10 @@ var HEX = 16;
|
|
|
1597
1597
|
var REG_BASE64 = /^[\da-z+/\-_=]+$/i;
|
|
1598
1598
|
var REG_DATA_URL = /data:[^,]*,[^"]+/g;
|
|
1599
1599
|
var REG_DATA_URL_BASE64 = /data:[^,]*;?base64,[\da-z+/\-_=]+/i;
|
|
1600
|
-
var REG_MIME_DOM = /^(?:text\/(?:ht|x)ml|application\/(?:xhtml\+)?xml|image\/svg\+xml)/;
|
|
1601
1600
|
var REG_HTML_SP = /[<>"'\s]/g;
|
|
1602
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))+?/;
|
|
1603
|
+
var REG_MIME_DOM = /^(?:text\/(?:ht|x)ml|application\/(?:xhtml\+)?xml|image\/svg\+xml)/;
|
|
1603
1604
|
var REG_NUM_REF = /&#(x(?:00)?[\dA-F]{2}|0?\d{1,3});?/ig;
|
|
1604
1605
|
var REG_SCHEME = /^[a-z][\da-z+\-.]*$/;
|
|
1605
1606
|
var REG_SCHEME_CUSTOM = /^(?:ext|web)\+[a-z]+$/;
|
|
@@ -1607,7 +1608,7 @@ var REG_SCRIPT = /(?:java|vb)script/;
|
|
|
1607
1608
|
var REG_URL_ENC = /^%[\dA-F]{2}$/i;
|
|
1608
1609
|
var REG_URL_ENC_AMP = /%26/g;
|
|
1609
1610
|
var REG_URL_ENC_HTML_SP = /%26(?:(?:l|g|quo)t|%2339);?/g;
|
|
1610
|
-
var
|
|
1611
|
+
var getURLEncodedString = (str) => {
|
|
1611
1612
|
if (!isString(str)) {
|
|
1612
1613
|
throw new TypeError(`Expected String but got ${getType(str)}.`);
|
|
1613
1614
|
}
|
|
@@ -1617,17 +1618,11 @@ var getUrlEncodedString = (str) => {
|
|
|
1617
1618
|
}
|
|
1618
1619
|
return chars.join("");
|
|
1619
1620
|
};
|
|
1620
|
-
var
|
|
1621
|
-
if (isString(ch)) {
|
|
1622
|
-
|
|
1623
|
-
ch = ch.toUpperCase();
|
|
1624
|
-
} else {
|
|
1625
|
-
throw new Error(`Invalid URL encoded character: ${ch}`);
|
|
1626
|
-
}
|
|
1627
|
-
} else {
|
|
1628
|
-
throw new TypeError(`Expected String but got ${getType(ch)}.`);
|
|
1621
|
+
var escapeURLEncodedHTMLChars = (ch) => {
|
|
1622
|
+
if (isString(ch) && REG_URL_ENC.test(ch)) {
|
|
1623
|
+
ch = ch.toUpperCase();
|
|
1629
1624
|
}
|
|
1630
|
-
const [amp, num, lt, gt, quot, apos] = ["&", "#", "<", ">", '"', "'"].map(
|
|
1625
|
+
const [amp, num, lt, gt, quot, apos] = ["&", "#", "<", ">", '"', "'"].map(getURLEncodedString);
|
|
1631
1626
|
let escapedChar;
|
|
1632
1627
|
if (ch === amp) {
|
|
1633
1628
|
escapedChar = `${amp}amp;`;
|
|
@@ -1644,7 +1639,7 @@ var escapeUrlEncodedHtmlChars = (ch) => {
|
|
|
1644
1639
|
}
|
|
1645
1640
|
return escapedChar;
|
|
1646
1641
|
};
|
|
1647
|
-
var
|
|
1642
|
+
var unescapeURLEncodedHTMLChars = (ch) => {
|
|
1648
1643
|
let unescapedChar;
|
|
1649
1644
|
if (/%26lt;?/.test(ch)) {
|
|
1650
1645
|
unescapedChar = "<";
|
|
@@ -1670,13 +1665,13 @@ var parseBase64 = (data) => {
|
|
|
1670
1665
|
const textCharCodes = new Set(text_chars_default);
|
|
1671
1666
|
let parsedData;
|
|
1672
1667
|
if (uint8arr.every((c) => textCharCodes.has(c))) {
|
|
1673
|
-
parsedData = bin.replace(/\s/g,
|
|
1668
|
+
parsedData = bin.replace(/\s/g, getURLEncodedString);
|
|
1674
1669
|
} else {
|
|
1675
1670
|
parsedData = data;
|
|
1676
1671
|
}
|
|
1677
1672
|
return parsedData;
|
|
1678
1673
|
};
|
|
1679
|
-
var
|
|
1674
|
+
var parseURLEncodedNumCharRef = (str, nest = 0) => {
|
|
1680
1675
|
if (!isString(str)) {
|
|
1681
1676
|
throw new TypeError(`Expected String but got ${getType(str)}.`);
|
|
1682
1677
|
}
|
|
@@ -1706,7 +1701,7 @@ var parseUrlEncodedNumCharRef = (str, nest = 0) => {
|
|
|
1706
1701
|
if (textCharCodes.has(num)) {
|
|
1707
1702
|
res = `${preNum}${String.fromCharCode(num)}${postNum}`;
|
|
1708
1703
|
if (/#x?$/.test(preNum) || /^#(?:x(?:00)?[2-7]|\d)/.test(postNum)) {
|
|
1709
|
-
res =
|
|
1704
|
+
res = parseURLEncodedNumCharRef(res, ++nest);
|
|
1710
1705
|
}
|
|
1711
1706
|
} else if (num < HEX * HEX) {
|
|
1712
1707
|
res = `${preNum}${postNum}`;
|
|
@@ -1716,7 +1711,7 @@ var parseUrlEncodedNumCharRef = (str, nest = 0) => {
|
|
|
1716
1711
|
}
|
|
1717
1712
|
return res;
|
|
1718
1713
|
};
|
|
1719
|
-
var
|
|
1714
|
+
var purifyURLEncodedDOM = (dom) => {
|
|
1720
1715
|
if (!isString(dom)) {
|
|
1721
1716
|
throw new TypeError(`Expected String but got ${getType(dom)}.`);
|
|
1722
1717
|
}
|
|
@@ -1777,12 +1772,12 @@ var URISchemes = class {
|
|
|
1777
1772
|
return this.#schemes.delete(scheme);
|
|
1778
1773
|
}
|
|
1779
1774
|
/**
|
|
1780
|
-
*
|
|
1775
|
+
* verify URI
|
|
1781
1776
|
*
|
|
1782
1777
|
* @param {string} uri - URI input
|
|
1783
1778
|
* @returns {boolean} - result
|
|
1784
1779
|
*/
|
|
1785
|
-
|
|
1780
|
+
verify(uri) {
|
|
1786
1781
|
let res;
|
|
1787
1782
|
if (isString(uri)) {
|
|
1788
1783
|
try {
|
|
@@ -1819,6 +1814,7 @@ var URLSanitizer = class extends URISchemes {
|
|
|
1819
1814
|
* @param {Array.<string>} opt.allow - array of allowed schemes
|
|
1820
1815
|
* @param {Array.<string>} opt.deny - array of denied schemes
|
|
1821
1816
|
* @param {Array.<string>} opt.only - array of specific schemes to allow
|
|
1817
|
+
* @param {boolean} opt.truncate - truncate tag and/or quote and the rest
|
|
1822
1818
|
* @returns {?string} - sanitized URL
|
|
1823
1819
|
*/
|
|
1824
1820
|
sanitize(url, opt = { allow: [], deny: [], only: [] }) {
|
|
@@ -1826,7 +1822,7 @@ var URLSanitizer = class extends URISchemes {
|
|
|
1826
1822
|
this.#nest = 0;
|
|
1827
1823
|
throw new Error("Data URLs nested too deeply.");
|
|
1828
1824
|
}
|
|
1829
|
-
const { allow, deny, only } = opt ?? {};
|
|
1825
|
+
const { allow, deny, only, truncate } = opt ?? {};
|
|
1830
1826
|
const schemeMap = /* @__PURE__ */ new Map([
|
|
1831
1827
|
["data", false],
|
|
1832
1828
|
["file", false],
|
|
@@ -1896,7 +1892,7 @@ var URLSanitizer = class extends URISchemes {
|
|
|
1896
1892
|
}
|
|
1897
1893
|
}
|
|
1898
1894
|
let sanitizedUrl;
|
|
1899
|
-
if (super.
|
|
1895
|
+
if (super.verify(url)) {
|
|
1900
1896
|
const { hash, href, pathname, protocol, search } = new URL(url);
|
|
1901
1897
|
const scheme = protocol.replace(/:$/, "");
|
|
1902
1898
|
const schemeParts = scheme.split("+");
|
|
@@ -1924,7 +1920,7 @@ var URLSanitizer = class extends URISchemes {
|
|
|
1924
1920
|
parsedData = parseBase64(data);
|
|
1925
1921
|
} else {
|
|
1926
1922
|
try {
|
|
1927
|
-
const decodedData =
|
|
1923
|
+
const decodedData = parseURLEncodedNumCharRef(parsedData);
|
|
1928
1924
|
const { protocol: dataScheme } = new URL(decodedData.trim());
|
|
1929
1925
|
const dataSchemeParts = dataScheme.replace(/:$/, "").split("+");
|
|
1930
1926
|
if (dataSchemeParts.some((s) => REG_SCRIPT.test(s))) {
|
|
@@ -1976,10 +1972,15 @@ var URLSanitizer = class extends URISchemes {
|
|
|
1976
1972
|
} else {
|
|
1977
1973
|
escapeHtml = true;
|
|
1978
1974
|
}
|
|
1975
|
+
if (!schemeParts.includes("data") && truncate && REG_HTML_SP_URL_ENC_SHORT.test(urlToSanitize)) {
|
|
1976
|
+
const item = REG_HTML_SP_URL_ENC_SHORT.exec(urlToSanitize);
|
|
1977
|
+
const { index } = item;
|
|
1978
|
+
urlToSanitize = urlToSanitize.substring(0, index);
|
|
1979
|
+
}
|
|
1979
1980
|
if (urlToSanitize) {
|
|
1980
|
-
sanitizedUrl = urlToSanitize.replace(REG_HTML_SP,
|
|
1981
|
+
sanitizedUrl = urlToSanitize.replace(REG_HTML_SP, getURLEncodedString).replace(REG_URL_ENC_AMP, escapeURLEncodedHTMLChars);
|
|
1981
1982
|
if (escapeHtml) {
|
|
1982
|
-
sanitizedUrl = sanitizedUrl.replace(REG_HTML_SP_URL_ENC,
|
|
1983
|
+
sanitizedUrl = sanitizedUrl.replace(REG_HTML_SP_URL_ENC, escapeURLEncodedHTMLChars);
|
|
1983
1984
|
this.#nest = 0;
|
|
1984
1985
|
}
|
|
1985
1986
|
} else {
|
|
@@ -1991,7 +1992,7 @@ var URLSanitizer = class extends URISchemes {
|
|
|
1991
1992
|
return sanitizedUrl || null;
|
|
1992
1993
|
}
|
|
1993
1994
|
/**
|
|
1994
|
-
* object with
|
|
1995
|
+
* object with additional properties based on URL API
|
|
1995
1996
|
*
|
|
1996
1997
|
* @typedef {object} ParsedURL
|
|
1997
1998
|
* @property {string} input - URL input
|
|
@@ -2018,7 +2019,7 @@ var URLSanitizer = class extends URISchemes {
|
|
|
2018
2019
|
*
|
|
2019
2020
|
* @param {string} url - URL input
|
|
2020
2021
|
* @param {object} opt - options
|
|
2021
|
-
* @returns {ParsedURL} - result with
|
|
2022
|
+
* @returns {ParsedURL} - result with enhanced props based on URL API
|
|
2022
2023
|
*/
|
|
2023
2024
|
parse(url, opt) {
|
|
2024
2025
|
if (!isString(url)) {
|
|
@@ -2058,10 +2059,10 @@ var URLSanitizer = class extends URISchemes {
|
|
|
2058
2059
|
parsedData.substring(0, index),
|
|
2059
2060
|
parsedData.substring(index + htmlChar.length)
|
|
2060
2061
|
];
|
|
2061
|
-
const
|
|
2062
|
-
parsedData = `${preHtmlChar}${
|
|
2062
|
+
const unescapedHtmlChar = unescapeURLEncodedHTMLChars(htmlChar);
|
|
2063
|
+
parsedData = `${preHtmlChar}${unescapedHtmlChar}${postHtmlChar}`;
|
|
2063
2064
|
}
|
|
2064
|
-
purifiedDom =
|
|
2065
|
+
purifiedDom = purifyURLEncodedDOM(parsedData);
|
|
2065
2066
|
dataUrl.set("data", purifiedDom);
|
|
2066
2067
|
} else {
|
|
2067
2068
|
dataUrl.set("data", data);
|
|
@@ -2100,12 +2101,17 @@ var URLSanitizer = class extends URISchemes {
|
|
|
2100
2101
|
}
|
|
2101
2102
|
};
|
|
2102
2103
|
var urlSanitizer = new URLSanitizer();
|
|
2103
|
-
var
|
|
2104
|
+
var isURISync = (uri) => urlSanitizer.verify(uri);
|
|
2104
2105
|
var isURI = async (uri) => {
|
|
2105
|
-
const res = await
|
|
2106
|
+
const res = await isURISync(uri);
|
|
2106
2107
|
return res;
|
|
2107
2108
|
};
|
|
2108
|
-
var
|
|
2109
|
+
var parseURLSync = (url) => urlSanitizer.parse(url);
|
|
2110
|
+
var parseURL = async (url) => {
|
|
2111
|
+
const res = await parseURLSync(url);
|
|
2112
|
+
return res;
|
|
2113
|
+
};
|
|
2114
|
+
var sanitizeURLSync = (url, opt) => {
|
|
2109
2115
|
const parsedUrl = urlSanitizer.parse(url, opt ?? {
|
|
2110
2116
|
allow: [],
|
|
2111
2117
|
deny: [],
|
|
@@ -2119,22 +2125,17 @@ var sanitizeUrl = (url, opt) => {
|
|
|
2119
2125
|
return res ?? null;
|
|
2120
2126
|
};
|
|
2121
2127
|
var sanitizeURL = async (url, opt) => {
|
|
2122
|
-
const res = await
|
|
2123
|
-
return res;
|
|
2124
|
-
};
|
|
2125
|
-
var parseUrl = (url) => urlSanitizer.parse(url);
|
|
2126
|
-
var parseURL = async (url) => {
|
|
2127
|
-
const res = await parseUrl(url);
|
|
2128
|
+
const res = await sanitizeURLSync(url, opt);
|
|
2128
2129
|
return res;
|
|
2129
2130
|
};
|
|
2130
2131
|
export {
|
|
2131
2132
|
urlSanitizer as default,
|
|
2132
2133
|
isURI,
|
|
2133
|
-
|
|
2134
|
+
isURISync,
|
|
2134
2135
|
parseURL,
|
|
2135
|
-
|
|
2136
|
+
parseURLSync,
|
|
2136
2137
|
sanitizeURL,
|
|
2137
|
-
|
|
2138
|
+
sanitizeURLSync
|
|
2138
2139
|
};
|
|
2139
2140
|
/*!
|
|
2140
2141
|
* URL Sanitizer
|