Skip to content

Commit

Permalink
Revised implementation (#77)
Browse files Browse the repository at this point in the history
Co-authored-by: Braintree <code@getbraintree.com>
  • Loading branch information
ibooker and braintreeps committed Jul 17, 2024
1 parent ec9925c commit 820d51c
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 7 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## UNRELEASED

- Update to handle back-slashes

## 7.0.4

- Updates get-func-name to 2.0.2
Expand Down
24 changes: 23 additions & 1 deletion src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe("sanitizeUrl", () => {
});

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

it("does not alter https URLs with ports with alphanumeric characters", () => {
Expand Down Expand Up @@ -147,6 +147,28 @@ describe("sanitizeUrl", () => {
});
});

it("backslash prefixed attack vectors", () => {
const attackVectors = [
"\fjavascript:alert()",
"\vjavascript:alert()",
"\tjavascript:alert()",
"\njavascript:alert()",
"\rjavascript:alert()",
"\u0000javascript:alert()",
"\u0001javascript:alert()",
];

attackVectors.forEach((vector) => {
expect(sanitizeUrl(vector)).toBe(BLANK_URL);
});
});

it("reverses backslashes", () => {
const attack = "\\j\\av\\a\\s\\cript:alert()";

expect(sanitizeUrl(attack)).toBe("/j/av/a/s/cript:alert()");
});

describe("invalid protocols", () => {
describe.each(["javascript", "data", "vbscript"])("%s", (protocol) => {
it(`replaces ${protocol} urls with ${BLANK_URL}`, () => {
Expand Down
40 changes: 34 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,25 @@ import {
htmlEntitiesRegex,
invalidProtocolRegex,
relativeFirstCharacters,
urlSchemeRegex,
whitespaceEscapeCharsRegex,
urlSchemeRegex,
} from "./constants";

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

// adapted from https://stackoverflow.com/a/29824550/2601552
function decodeHtmlCharacters(str: string) {
const removedNullByte = str.replace(ctrlCharactersRegex, "");
return removedNullByte.replace(htmlEntitiesRegex, (match, dec) => {
return String.fromCharCode(dec);
});
}

function isValidUrl(url: string): boolean {
return URL.canParse(url);
}

function decodeURI(uri: string): string {
try {
return decodeURIComponent(uri);
Expand All @@ -36,8 +39,9 @@ export function sanitizeUrl(url?: string): string {
if (!url) {
return BLANK_URL;
}

let charsToDecode;
let decodedUrl = decodeURI(url);
let decodedUrl = decodeURI(url.trim());

do {
decodedUrl = decodeHtmlCharacters(decodedUrl)
Expand All @@ -54,7 +58,9 @@ export function sanitizeUrl(url?: string): string {
decodedUrl.match(htmlCtrlEntityRegex) ||
decodedUrl.match(whitespaceEscapeCharsRegex);
} while (charsToDecode && charsToDecode.length > 0);

const sanitizedUrl = decodedUrl;

if (!sanitizedUrl) {
return BLANK_URL;
}
Expand All @@ -63,17 +69,39 @@ export function sanitizeUrl(url?: string): string {
return sanitizedUrl;
}

const urlSchemeParseResults = sanitizedUrl.match(urlSchemeRegex);
// Remove any leading whitespace before checking the URL scheme
const trimmedUrl = sanitizedUrl.trimStart();
const urlSchemeParseResults = trimmedUrl.match(urlSchemeRegex);

if (!urlSchemeParseResults) {
return sanitizedUrl;
}

const urlScheme = urlSchemeParseResults[0];
const urlScheme = urlSchemeParseResults[0].toLowerCase().trim();

if (invalidProtocolRegex.test(urlScheme)) {
return BLANK_URL;
}

return sanitizedUrl;
const backSanitized = trimmedUrl.replace(/\\/g, "/");

// Handle special cases for mailto: and custom deep-link protocols
if (urlScheme === "mailto:" || urlScheme.includes("://")) {
return backSanitized;
}

// For http and https URLs, perform additional validation
if (urlScheme === "http:" || urlScheme === "https:") {
if (!isValidUrl(backSanitized)) {
return BLANK_URL;
}

const url = new URL(backSanitized);
url.protocol = url.protocol.toLowerCase();
url.hostname = url.hostname.toLowerCase();

return url.toString();
}

return backSanitized;
}

0 comments on commit 820d51c

Please sign in to comment.