@ez1219/sanitize-url 7.1.2-rc.0

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 ADDED
@@ -0,0 +1,39 @@
1
+ # sanitize-url
2
+
3
+ ## Installation
4
+
5
+ ```sh
6
+ npm install -S @ez1219/sanitize-url
7
+ ```
8
+
9
+ ## Usage
10
+
11
+ ```js
12
+ var sanitizeUrl = require("@ez1219/sanitize-url").sanitizeUrl;
13
+
14
+ sanitizeUrl("https://example.com"); // 'https://example.com'
15
+ sanitizeUrl("http://example.com"); // 'http://example.com'
16
+ sanitizeUrl("www.example.com"); // 'www.example.com'
17
+ sanitizeUrl("mailto:hello@example.com"); // 'mailto:hello@example.com'
18
+ sanitizeUrl(
19
+ "https&#0000058//example.com",
20
+ ); // https://example.com
21
+
22
+ sanitizeUrl("javascript:alert(document.domain)"); // 'about:blank'
23
+ sanitizeUrl("jAvasCrIPT:alert(document.domain)"); // 'about:blank'
24
+ sanitizeUrl(decodeURIComponent("JaVaScRiP%0at:alert(document.domain)")); // 'about:blank'
25
+ // HTML encoded javascript:alert('XSS')
26
+ sanitizeUrl(
27
+ "&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041",
28
+ ); // 'about:blank'
29
+ ```
30
+
31
+ ## Testing
32
+
33
+ This library uses [Vitest](https://vitest.dev/). All testing dependencies
34
+ will be installed upon `npm install` and the test suite can be executed with
35
+ `npm test`. Running the test suite will also run lint checks upon exiting.
36
+
37
+ npm test
38
+
39
+ To generate a coverage report, use `npm run coverage`.
package/SECURITY.md ADDED
@@ -0,0 +1,41 @@
1
+ # Security Policy
2
+
3
+ This repository adheres to the [PayPal Vulnerability Reporting Policy](https://hackerone.com/paypal).
4
+
5
+ ## Reporting a Vulnerability
6
+
7
+ If you think you have found a vulnerability in this repository, please report it to us through coordinated disclosure.
8
+
9
+ **Please do not report security vulnerabilities through public issues, discussions, or pull requests.**
10
+
11
+ Instead, report it using one of the following ways:
12
+
13
+ - Email the PayPal Security Team at [security@paypal.com](mailto:security@paypal.com)
14
+ - Submit through the [PayPal Bug Bounty Program](https://hackerone.com/paypal) on HackerOne
15
+ - Report a [vulnerability](https://github.com/ez1219/sanitize-url/security/advisories/new) directly via private vulnerability reporting on GitHub
16
+
17
+ Please include the following in your report:
18
+
19
+ - The type of issue and affected version(s)
20
+ - Step-by-step instructions to reproduce the issue
21
+ - Impact of the issue and how an attacker might exploit it
22
+
23
+ ## Supported Security Updates
24
+
25
+ Only the latest release receives security patches and new versions in the case of a security issue.
26
+
27
+ ## Disclosure Policy
28
+
29
+ We are committed to working with security researchers in good faith. To support responsible disclosure, our team will:
30
+
31
+ - Acknowledge your report in a timely manner
32
+ - Keep you informed of our progress toward a fix
33
+ - Notify you before any public disclosure
34
+
35
+ We ask that you:
36
+
37
+ - Do not publicly disclose the issue before it has been resolved
38
+ - Avoid accessing, modifying, or deleting data that does not belong to you
39
+ - Make a good faith effort to avoid disruption to production systems
40
+
41
+ We appreciate responsible disclosure and your efforts to keep Braintree SDK users safe.
@@ -0,0 +1,70 @@
1
+ // commitlint.config.js
2
+ module.exports = {
3
+ extends: ["@commitlint/config-conventional"],
4
+
5
+ // Customize rules
6
+ rules: {
7
+ // Type must be one of the specified values
8
+ "type-enum": [
9
+ 2, // Error level (0=off, 1=warn, 2=error)
10
+ "always",
11
+ [
12
+ "feat", // New feature
13
+ "fix", // Bug fix
14
+ "review",
15
+ "docs", // Documentation
16
+ "style", // Formatting
17
+ "refactor", // Code restructuring
18
+ "perf", // Performance
19
+ "test", // Tests
20
+ "build", // Build system
21
+ "dx", // Developer experience
22
+ "ci", // CI configuration
23
+ "chore", // Maintenance
24
+ "revert", // Revert commit
25
+ ],
26
+ ],
27
+
28
+ "scope-enum": [
29
+ 2,
30
+ "always",
31
+ [
32
+ "american-express",
33
+ "apple-pay",
34
+ "client",
35
+ "data-collector",
36
+ "fastlane",
37
+ "google-payment",
38
+ "hosted-fields",
39
+ "instant-verification",
40
+ "local-payment",
41
+ "payment-ready",
42
+ "payment-request",
43
+ "paypal-checkout",
44
+ "paypal-checkout-v6",
45
+ "sepa",
46
+ "three-d-secure",
47
+ "us-bank-account",
48
+ "vault-manager",
49
+ "venmo",
50
+ "deps",
51
+ "dev-deps",
52
+ "other",
53
+ ],
54
+ ],
55
+ // Scope is optional but recommended
56
+ "scope-empty": [1, "never"],
57
+
58
+ // Subject configuration
59
+ "subject-empty": [2, "never"],
60
+ "subject-case": [0, "always", "sentence-case"],
61
+ "subject-full-stop": [1, "never", "."],
62
+ "subject-max-length": [2, "always", 85],
63
+
64
+ // Body configuration
65
+ "body-max-line-length": [2, "always", 100],
66
+
67
+ // Footer configuration
68
+ "footer-max-line-length": [2, "always", 100],
69
+ },
70
+ };
@@ -0,0 +1,8 @@
1
+ export declare const invalidProtocolRegex: RegExp;
2
+ export declare const htmlEntitiesRegex: RegExp;
3
+ export declare const htmlCtrlEntityRegex: RegExp;
4
+ export declare const ctrlCharactersRegex: RegExp;
5
+ export declare const urlSchemeRegex: RegExp;
6
+ export declare const whitespaceEscapeCharsRegex: RegExp;
7
+ export declare const relativeFirstCharacters: string[];
8
+ export declare const BLANK_URL = "about:blank";
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BLANK_URL = exports.relativeFirstCharacters = exports.whitespaceEscapeCharsRegex = exports.urlSchemeRegex = exports.ctrlCharactersRegex = exports.htmlCtrlEntityRegex = exports.htmlEntitiesRegex = exports.invalidProtocolRegex = void 0;
4
+ exports.invalidProtocolRegex = /^([^\w]*)(javascript|data|vbscript)/im;
5
+ exports.htmlEntitiesRegex = /&#(\w+)(^\w|;)?/g;
6
+ exports.htmlCtrlEntityRegex = /&(newline|tab);/gi;
7
+ exports.ctrlCharactersRegex = /[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim;
8
+ exports.urlSchemeRegex = /^.+(:|:)/gim;
9
+ exports.whitespaceEscapeCharsRegex = /(\\|%5[cC])((%(6[eE]|72|74))|[nrt])/g;
10
+ exports.relativeFirstCharacters = [".", "/"];
11
+ exports.BLANK_URL = "about:blank";
@@ -0,0 +1 @@
1
+ export declare function sanitizeUrl(url?: string): string;
package/dist/index.js ADDED
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeUrl = sanitizeUrl;
4
+ var constants_1 = require("./constants");
5
+ function isRelativeUrlWithoutProtocol(url) {
6
+ return constants_1.relativeFirstCharacters.indexOf(url[0]) > -1;
7
+ }
8
+ function decodeHtmlCharacters(str) {
9
+ var removedNullByte = str.replace(constants_1.ctrlCharactersRegex, "");
10
+ return removedNullByte.replace(constants_1.htmlEntitiesRegex, function (match, dec) {
11
+ return String.fromCharCode(dec);
12
+ });
13
+ }
14
+ function isValidUrl(url) {
15
+ if (typeof URL !== "undefined" && typeof URL.canParse === "function") {
16
+ return URL.canParse(url);
17
+ }
18
+ try {
19
+ return Boolean(new URL(url));
20
+ }
21
+ catch (_a) {
22
+ return false;
23
+ }
24
+ }
25
+ function decodeURI(uri) {
26
+ try {
27
+ return decodeURIComponent(uri);
28
+ }
29
+ catch (e) {
30
+ // Ignoring error
31
+ // It is possible that the URI contains a `%` not associated
32
+ // with URI/URL-encoding.
33
+ return uri;
34
+ }
35
+ }
36
+ function sanitizeUrl(url) {
37
+ if (!url) {
38
+ return constants_1.BLANK_URL;
39
+ }
40
+ var charsToDecode;
41
+ var decodedUrl = decodeURI(url.trim());
42
+ do {
43
+ decodedUrl = decodeHtmlCharacters(decodedUrl)
44
+ .replace(constants_1.htmlCtrlEntityRegex, "")
45
+ .replace(constants_1.ctrlCharactersRegex, "")
46
+ .replace(constants_1.whitespaceEscapeCharsRegex, "")
47
+ .trim();
48
+ decodedUrl = decodeURI(decodedUrl);
49
+ charsToDecode =
50
+ decodedUrl.match(constants_1.ctrlCharactersRegex) ||
51
+ decodedUrl.match(constants_1.htmlEntitiesRegex) ||
52
+ decodedUrl.match(constants_1.htmlCtrlEntityRegex) ||
53
+ decodedUrl.match(constants_1.whitespaceEscapeCharsRegex);
54
+ } while (charsToDecode && charsToDecode.length > 0);
55
+ var sanitizedUrl = decodedUrl;
56
+ if (!sanitizedUrl) {
57
+ return constants_1.BLANK_URL;
58
+ }
59
+ if (isRelativeUrlWithoutProtocol(sanitizedUrl)) {
60
+ return sanitizedUrl;
61
+ }
62
+ // Remove any leading whitespace before checking the URL scheme
63
+ var trimmedUrl = sanitizedUrl.trimStart();
64
+ var urlSchemeParseResults = trimmedUrl.match(constants_1.urlSchemeRegex);
65
+ if (!urlSchemeParseResults) {
66
+ return sanitizedUrl;
67
+ }
68
+ var urlScheme = urlSchemeParseResults[0].toLowerCase().trim();
69
+ if (constants_1.invalidProtocolRegex.test(urlScheme)) {
70
+ return constants_1.BLANK_URL;
71
+ }
72
+ var backSanitized = trimmedUrl.replace(/\\/g, "/");
73
+ // Handle special cases for mailto: and custom deep-link protocols
74
+ if (urlScheme === "mailto:" || urlScheme.includes("://")) {
75
+ return backSanitized;
76
+ }
77
+ // For http and https URLs, perform additional validation
78
+ if (urlScheme === "http:" || urlScheme === "https:") {
79
+ if (!isValidUrl(backSanitized)) {
80
+ return constants_1.BLANK_URL;
81
+ }
82
+ var url_1 = new URL(backSanitized);
83
+ url_1.protocol = url_1.protocol.toLowerCase();
84
+ url_1.hostname = url_1.hostname.toLowerCase();
85
+ return url_1.toString();
86
+ }
87
+ return backSanitized;
88
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@ez1219/sanitize-url",
3
+ "version": "7.1.2-rc.0",
4
+ "description": "A url sanitizer",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "author": "",
8
+ "scripts": {
9
+ "prepublishOnly": "npm run build",
10
+ "prebuild": "prettier --write .",
11
+ "build": "tsc --declaration",
12
+ "lint": "eslint --ext js,ts .",
13
+ "posttest": "npm run lint",
14
+ "test": "vitest",
15
+ "coverage": "vitest run --coverage",
16
+ "prepare": "husky",
17
+ "commit": "cz"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/ez1219/sanitize-url.git"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "keywords": [],
27
+ "license": "MIT",
28
+ "bugs": {
29
+ "url": "https://github.com/ez1219/sanitize-url/issues"
30
+ },
31
+ "homepage": "https://github.com/ez1219/sanitize-url#readme",
32
+ "devDependencies": {
33
+ "@commitlint/cli": "^20.5.0",
34
+ "@commitlint/config-conventional": "^20.5.0",
35
+ "@types/jest": "^30.0.0",
36
+ "@types/node": "^24.0.0",
37
+ "@typescript-eslint/eslint-plugin": "^5.54.1",
38
+ "@vitest/coverage-v8": "^4.0.16",
39
+ "chai": "^6.2.2",
40
+ "commitizen": "^4.3.1",
41
+ "cz-customizable": "^7.5.4",
42
+ "eslint": "^8.36.0",
43
+ "eslint-config-braintree": "^6.0.0-typescript-prep-rc.2",
44
+ "eslint-plugin-prettier": "^5.5.4",
45
+ "happy-dom": "^20.0.11",
46
+ "husky": "^9.1.7",
47
+ "prettier": "^3.7.4",
48
+ "typescript": "^5.9.3",
49
+ "vitest": "^4.0.16"
50
+ },
51
+ "config": {
52
+ "commitizen": {
53
+ "path": "./node_modules/cz-customizable"
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,299 @@
1
+ /* eslint-disable no-script-url */
2
+ import { sanitizeUrl } from "..";
3
+ import { BLANK_URL } from "../constants";
4
+
5
+ describe("sanitizeUrl", () => {
6
+ it("does not alter http URLs with alphanumeric characters", () => {
7
+ expect(sanitizeUrl("http://example.com/path/to:something")).toBe(
8
+ "http://example.com/path/to:something",
9
+ );
10
+ });
11
+
12
+ it("does not alter http URLs with ports with alphanumeric characters", () => {
13
+ expect(sanitizeUrl("http://example.com:4567/path/to:something")).toBe(
14
+ "http://example.com:4567/path/to:something",
15
+ );
16
+ });
17
+
18
+ it("does not alter https URLs with alphanumeric characters", () => {
19
+ expect(sanitizeUrl("https://example.com")).toBe("https://example.com/");
20
+ });
21
+
22
+ it("does not alter https URLs with ports with alphanumeric characters", () => {
23
+ expect(sanitizeUrl("https://example.com:4567/path/to:something")).toBe(
24
+ "https://example.com:4567/path/to:something",
25
+ );
26
+ });
27
+
28
+ it("does not alter relative-path reference URLs with alphanumeric characters", () => {
29
+ expect(sanitizeUrl("./path/to/my.json")).toBe("./path/to/my.json");
30
+ });
31
+
32
+ it("does not alter absolute-path reference URLs with alphanumeric characters", () => {
33
+ expect(sanitizeUrl("/path/to/my.json")).toBe("/path/to/my.json");
34
+ });
35
+
36
+ it("does not alter protocol-less network-path URLs with alphanumeric characters", () => {
37
+ expect(sanitizeUrl("//google.com/robots.txt")).toBe(
38
+ "//google.com/robots.txt",
39
+ );
40
+ });
41
+
42
+ it("does not alter protocol-less URLs with alphanumeric characters", () => {
43
+ expect(sanitizeUrl("www.example.com")).toBe("www.example.com");
44
+ });
45
+
46
+ it("does not alter deep-link urls with alphanumeric characters", () => {
47
+ expect(sanitizeUrl("com.braintreepayments.demo://example")).toBe(
48
+ "com.braintreepayments.demo://example",
49
+ );
50
+ });
51
+
52
+ it("does not alter mailto urls with alphanumeric characters", () => {
53
+ expect(sanitizeUrl("mailto:test@example.com?subject=hello+world")).toBe(
54
+ "mailto:test@example.com?subject=hello+world",
55
+ );
56
+ });
57
+
58
+ it("does not alter urls with accented characters", () => {
59
+ expect(sanitizeUrl("www.example.com/with-áccêntš")).toBe(
60
+ "www.example.com/with-áccêntš",
61
+ );
62
+ });
63
+
64
+ it("does not strip harmless unicode characters", () => {
65
+ expect(sanitizeUrl("www.example.com/лот.рфшишкиü–")).toBe(
66
+ "www.example.com/лот.рфшишкиü–",
67
+ );
68
+ });
69
+
70
+ it("strips out ctrl chars", () => {
71
+ expect(
72
+ sanitizeUrl("www.example.com/\u200D\u0000\u001F\x00\x1F\uFEFFfoo"),
73
+ ).toBe("www.example.com/foo");
74
+ });
75
+
76
+ it(`replaces blank urls with ${BLANK_URL}`, () => {
77
+ expect(sanitizeUrl("")).toBe(BLANK_URL);
78
+ });
79
+
80
+ it(`replaces null values with ${BLANK_URL}`, () => {
81
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
82
+ // @ts-ignore
83
+ expect(sanitizeUrl(null)).toBe(BLANK_URL);
84
+ });
85
+
86
+ it(`replaces undefined values with ${BLANK_URL}`, () => {
87
+ expect(sanitizeUrl()).toBe(BLANK_URL);
88
+ });
89
+
90
+ it("removes whitespace from urls", () => {
91
+ expect(sanitizeUrl(" http://example.com/path/to:something ")).toBe(
92
+ "http://example.com/path/to:something",
93
+ );
94
+ });
95
+
96
+ it("removes newline entities from urls", () => {
97
+ expect(sanitizeUrl("https://example.com

/something")).toBe(
98
+ "https://example.com/something",
99
+ );
100
+ });
101
+
102
+ it("decodes html entities", () => {
103
+ // all these decode to javascript:alert('xss');
104
+ const attackVectors = [
105
+ "&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041",
106
+ "javascript:alert('XSS')",
107
+ "&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29",
108
+ "jav	ascript:alert('XSS');",
109
+ "  javascript:alert('XSS');",
110
+ "javasc	ript: alert('XSS');",
111
+ "javasc&#\u0000x09;ript:alert(1)",
112
+ "java&&#78&#59;ewLine&newline&#59;&#59;script:alert('XSS')",
113
+ "java&NewLine&newline;;script:alert('XSS')",
114
+ ];
115
+
116
+ attackVectors.forEach((vector) => {
117
+ expect(sanitizeUrl(vector)).toBe(BLANK_URL);
118
+ });
119
+
120
+ // https://example.com/javascript:alert('XSS')
121
+ // since the javascript is the url path, and not the protocol,
122
+ // this url is technically sanitized
123
+ expect(
124
+ sanitizeUrl(
125
+ "https&#0000058//example.com/&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041",
126
+ ),
127
+ ).toBe("https://example.com/javascript:alert('XSS')");
128
+ });
129
+
130
+ it("removes whitespace escape sequences", () => {
131
+ const attackVectors = [
132
+ "javascri\npt:alert('xss')",
133
+ "javascri\rpt:alert('xss')",
134
+ "javascri\tpt:alert('xss')",
135
+ "javascrip\\%74t:alert('XSS')",
136
+ "javascrip%5c%72t:alert()",
137
+ "javascrip%5Ctt:alert()",
138
+ "javascrip%255Ctt:alert()",
139
+ "javascrip%25%35Ctt:alert()",
140
+ "javascrip%25%35%43tt:alert()",
141
+ "javascrip%25%32%35%25%33%35%25%34%33rt:alert()",
142
+ "javascrip%255Crt:alert('%25xss')",
143
+ ];
144
+
145
+ attackVectors.forEach((vector) => {
146
+ expect(sanitizeUrl(vector)).toBe(BLANK_URL);
147
+ });
148
+ });
149
+
150
+ it("backslash prefixed attack vectors", () => {
151
+ const attackVectors = [
152
+ "\fjavascript:alert()",
153
+ "\vjavascript:alert()",
154
+ "\tjavascript:alert()",
155
+ "\njavascript:alert()",
156
+ "\rjavascript:alert()",
157
+ "\u0000javascript:alert()",
158
+ "\u0001javascript:alert()",
159
+ ];
160
+
161
+ attackVectors.forEach((vector) => {
162
+ expect(sanitizeUrl(vector)).toBe(BLANK_URL);
163
+ });
164
+ });
165
+
166
+ it("reverses backslashes", () => {
167
+ const attack = "\\j\\av\\a\\s\\cript:alert()";
168
+
169
+ expect(sanitizeUrl(attack)).toBe("/j/av/a/s/cript:alert()");
170
+ });
171
+
172
+ describe("invalid protocols", () => {
173
+ describe.each(["javascript", "data", "vbscript"])("%s", (protocol) => {
174
+ it(`replaces ${protocol} urls with ${BLANK_URL}`, () => {
175
+ expect(sanitizeUrl(`${protocol}:alert(document.domain)`)).toBe(
176
+ BLANK_URL,
177
+ );
178
+ });
179
+
180
+ it(`allows ${protocol} urls that start with a letter prefix`, () => {
181
+ expect(sanitizeUrl(`not_${protocol}:alert(document.domain)`)).toBe(
182
+ `not_${protocol}:alert(document.domain)`,
183
+ );
184
+ });
185
+
186
+ it(`disallows ${protocol} urls that start with non-\w characters as a suffix for the protocol`, () => {
187
+ expect(sanitizeUrl(`&!*${protocol}:alert(document.domain)`)).toBe(
188
+ BLANK_URL,
189
+ );
190
+ });
191
+
192
+ it(`disallows ${protocol} urls that use : for the colon portion of the url`, () => {
193
+ expect(sanitizeUrl(`${protocol}:alert(document.domain)`)).toBe(
194
+ BLANK_URL,
195
+ );
196
+ expect(sanitizeUrl(`${protocol}:alert(document.domain)`)).toBe(
197
+ BLANK_URL,
198
+ );
199
+ });
200
+
201
+ it(`disregards capitalization for ${protocol} urls`, () => {
202
+ // upper case every other letter in protocol name
203
+ const mixedCapitalizationProtocol = protocol
204
+ .split("")
205
+ .map((character, index) => {
206
+ if (index % 2 === 0) {
207
+ return character.toUpperCase();
208
+ }
209
+ return character;
210
+ })
211
+ .join("");
212
+
213
+ expect(
214
+ sanitizeUrl(`${mixedCapitalizationProtocol}:alert(document.domain)`),
215
+ ).toBe(BLANK_URL);
216
+ });
217
+
218
+ it(`ignores invisible ctrl characters in ${protocol} urls`, () => {
219
+ const protocolWithControlCharacters = protocol
220
+ .split("")
221
+ .map((character, index) => {
222
+ if (index === 1) {
223
+ return character + "%EF%BB%BF%EF%BB%BF";
224
+ } else if (index === 2) {
225
+ return character + "%e2%80%8b";
226
+ }
227
+ return character;
228
+ })
229
+ .join("");
230
+
231
+ expect(
232
+ sanitizeUrl(
233
+ decodeURIComponent(
234
+ `${protocolWithControlCharacters}:alert(document.domain)`,
235
+ ),
236
+ ),
237
+ ).toBe(BLANK_URL);
238
+ });
239
+
240
+ it(`replaces ${protocol} urls with ${BLANK_URL} when url begins with %20`, () => {
241
+ expect(
242
+ sanitizeUrl(
243
+ decodeURIComponent(
244
+ `%20%20%20%20${protocol}:alert(document.domain)`,
245
+ ),
246
+ ),
247
+ ).toBe(BLANK_URL);
248
+ });
249
+
250
+ it(`replaces ${protocol} urls with ${BLANK_URL} when ${protocol} url begins with spaces`, () => {
251
+ expect(sanitizeUrl(` ${protocol}:alert(document.domain)`)).toBe(
252
+ BLANK_URL,
253
+ );
254
+ });
255
+
256
+ it(`does not replace ${protocol}: if it is not in the scheme of the URL`, () => {
257
+ expect(sanitizeUrl(`http://example.com#${protocol}:foo`)).toBe(
258
+ `http://example.com#${protocol}:foo`,
259
+ );
260
+ });
261
+ });
262
+ });
263
+
264
+ it("replaces invalid http/https URLs with BLANK_URL", () => {
265
+ expect(sanitizeUrl("http://[invalid]")).toBe(BLANK_URL);
266
+ expect(sanitizeUrl("https://[invalid]")).toBe(BLANK_URL);
267
+ });
268
+
269
+ describe("when URL.canParse is undefined", () => {
270
+ let originalCanParse: typeof URL.canParse;
271
+
272
+ beforeEach(() => {
273
+ originalCanParse = URL.canParse;
274
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
275
+ // @ts-ignore
276
+ URL.canParse = null;
277
+ });
278
+
279
+ afterEach(() => {
280
+ URL.canParse = originalCanParse;
281
+ });
282
+
283
+ it("sanitizes valid http/https URLs properly using fallback", () => {
284
+ expect(sanitizeUrl("http://example.com/path")).toBe(
285
+ "http://example.com/path",
286
+ );
287
+ expect(sanitizeUrl("https://example.com")).toBe("https://example.com/");
288
+ });
289
+
290
+ it("replaces invalid http/https URLs with BLANK_URL using fallback", () => {
291
+ expect(sanitizeUrl("http://[invalid]")).toBe(BLANK_URL);
292
+ expect(sanitizeUrl("https://[invalid]")).toBe(BLANK_URL);
293
+ });
294
+
295
+ it("replaces javascript URLs with BLANK_URL", () => {
296
+ expect(sanitizeUrl("javascript:alert(1)")).toBe(BLANK_URL);
297
+ });
298
+ });
299
+ });
@@ -0,0 +1,10 @@
1
+ export const invalidProtocolRegex = /^([^\w]*)(javascript|data|vbscript)/im;
2
+ export const htmlEntitiesRegex = /&#(\w+)(^\w|;)?/g;
3
+ export const htmlCtrlEntityRegex = /&(newline|tab);/gi;
4
+ export const ctrlCharactersRegex =
5
+ /[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim;
6
+ export const urlSchemeRegex = /^.+(:|:)/gim;
7
+ export const whitespaceEscapeCharsRegex =
8
+ /(\\|%5[cC])((%(6[eE]|72|74))|[nrt])/g;
9
+ export const relativeFirstCharacters = [".", "/"];
10
+ export const BLANK_URL = "about:blank";