Skip to content

Commit 820d51c

Browse files
ibookerbraintreeps
andauthored
Revised implementation (#77)
Co-authored-by: Braintree <[email protected]>
1 parent ec9925c commit 820d51c

File tree

3 files changed

+61
-7
lines changed

3 files changed

+61
-7
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## UNRELEASED
4+
5+
- Update to handle back-slashes
6+
37
## 7.0.4
48

59
- Updates get-func-name to 2.0.2

src/__tests__/index.test.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe("sanitizeUrl", () => {
1616
});
1717

1818
it("does not alter https URLs with alphanumeric characters", () => {
19-
expect(sanitizeUrl("https://example.com")).toBe("https://example.com");
19+
expect(sanitizeUrl("https://example.com")).toBe("https://example.com/");
2020
});
2121

2222
it("does not alter https URLs with ports with alphanumeric characters", () => {
@@ -147,6 +147,28 @@ describe("sanitizeUrl", () => {
147147
});
148148
});
149149

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+
150172
describe("invalid protocols", () => {
151173
describe.each(["javascript", "data", "vbscript"])("%s", (protocol) => {
152174
it(`replaces ${protocol} urls with ${BLANK_URL}`, () => {

src/index.ts

+34-6
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,25 @@ import {
55
htmlEntitiesRegex,
66
invalidProtocolRegex,
77
relativeFirstCharacters,
8-
urlSchemeRegex,
98
whitespaceEscapeCharsRegex,
9+
urlSchemeRegex,
1010
} from "./constants";
1111

1212
function isRelativeUrlWithoutProtocol(url: string): boolean {
1313
return relativeFirstCharacters.indexOf(url[0]) > -1;
1414
}
1515

16-
// adapted from https://stackoverflow.com/a/29824550/2601552
1716
function decodeHtmlCharacters(str: string) {
1817
const removedNullByte = str.replace(ctrlCharactersRegex, "");
1918
return removedNullByte.replace(htmlEntitiesRegex, (match, dec) => {
2019
return String.fromCharCode(dec);
2120
});
2221
}
2322

23+
function isValidUrl(url: string): boolean {
24+
return URL.canParse(url);
25+
}
26+
2427
function decodeURI(uri: string): string {
2528
try {
2629
return decodeURIComponent(uri);
@@ -36,8 +39,9 @@ export function sanitizeUrl(url?: string): string {
3639
if (!url) {
3740
return BLANK_URL;
3841
}
42+
3943
let charsToDecode;
40-
let decodedUrl = decodeURI(url);
44+
let decodedUrl = decodeURI(url.trim());
4145

4246
do {
4347
decodedUrl = decodeHtmlCharacters(decodedUrl)
@@ -54,7 +58,9 @@ export function sanitizeUrl(url?: string): string {
5458
decodedUrl.match(htmlCtrlEntityRegex) ||
5559
decodedUrl.match(whitespaceEscapeCharsRegex);
5660
} while (charsToDecode && charsToDecode.length > 0);
61+
5762
const sanitizedUrl = decodedUrl;
63+
5864
if (!sanitizedUrl) {
5965
return BLANK_URL;
6066
}
@@ -63,17 +69,39 @@ export function sanitizeUrl(url?: string): string {
6369
return sanitizedUrl;
6470
}
6571

66-
const urlSchemeParseResults = sanitizedUrl.match(urlSchemeRegex);
72+
// Remove any leading whitespace before checking the URL scheme
73+
const trimmedUrl = sanitizedUrl.trimStart();
74+
const urlSchemeParseResults = trimmedUrl.match(urlSchemeRegex);
6775

6876
if (!urlSchemeParseResults) {
6977
return sanitizedUrl;
7078
}
7179

72-
const urlScheme = urlSchemeParseResults[0];
80+
const urlScheme = urlSchemeParseResults[0].toLowerCase().trim();
7381

7482
if (invalidProtocolRegex.test(urlScheme)) {
7583
return BLANK_URL;
7684
}
7785

78-
return sanitizedUrl;
86+
const backSanitized = trimmedUrl.replace(/\\/g, "/");
87+
88+
// Handle special cases for mailto: and custom deep-link protocols
89+
if (urlScheme === "mailto:" || urlScheme.includes("://")) {
90+
return backSanitized;
91+
}
92+
93+
// For http and https URLs, perform additional validation
94+
if (urlScheme === "http:" || urlScheme === "https:") {
95+
if (!isValidUrl(backSanitized)) {
96+
return BLANK_URL;
97+
}
98+
99+
const url = new URL(backSanitized);
100+
url.protocol = url.protocol.toLowerCase();
101+
url.hostname = url.hostname.toLowerCase();
102+
103+
return url.toString();
104+
}
105+
106+
return backSanitized;
79107
}

0 commit comments

Comments
 (0)