From 6dfe2134888682b745330887f03ae8504470e2e7 Mon Sep 17 00:00:00 2001 From: prashantjagasia <108553844+prashantjagasia@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:02:28 -0700 Subject: [PATCH] feat: added url-sanitization function (#110) * feat: added url-sanitization function * chore: fixed regex const * chore: added constants and updated code * chore: added tests, fixed build issues * chore: removed modified dist files --- src/constants.js | 12 ++++++++ src/util.js | 54 +++++++++++++++++++++++++++++++++- test/tests/util/index.js | 1 + test/tests/util/sanitizeUrl.js | 31 +++++++++++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 test/tests/util/sanitizeUrl.js diff --git a/src/constants.js b/src/constants.js index b2d3144..752e0e1 100644 --- a/src/constants.js +++ b/src/constants.js @@ -10,3 +10,15 @@ export const ATTRIBUTES = { }; export const UID_HASH_LENGTH = 30; + +/* eslint-disable no-control-regex*/ +export const invalidProtocolRegex: RegExp = + /([^\w]*)(javascript|data|vbscript)/im; +export const htmlEntitiesRegex: RegExp = /&#(\w+)(^\w|;)?/g; +export const htmlCtrlEntityRegex: RegExp = /&(newline|tab);/gi; +export const ctrlCharactersRegex: RegExp = + /[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim; +export const urlSchemeRegex: RegExp = /^.+(:|:)/gim; +export const relativeFirstCharacters = [".", "/"]; +export const BLANK_URL = "about:blank"; +/* eslint-enable no-control-regex*/ diff --git a/src/util.js b/src/util.js index d490755..27c462d 100644 --- a/src/util.js +++ b/src/util.js @@ -1,9 +1,17 @@ /* @flow */ /* eslint max-lines: 0 */ - import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; import { WeakMap } from "@krakenjs/cross-domain-safe-weakmap/src"; +import { + BLANK_URL, + ctrlCharactersRegex, + htmlCtrlEntityRegex, + htmlEntitiesRegex, + invalidProtocolRegex, + relativeFirstCharacters, + urlSchemeRegex, +} from "./constants"; import type { CancelableType } from "./types"; export function isElement(element: mixed): boolean { @@ -1370,3 +1378,47 @@ export class ExtendableError extends Error { } } } + +function isRelativeUrlWithoutProtocol(url: string): boolean { + return relativeFirstCharacters.indexOf(url[0]) > -1; +} + +function decodeHtmlCharacters(str: string): string { + const removedNullByte: string = str.replace(ctrlCharactersRegex, ""); + return removedNullByte.replace(htmlEntitiesRegex, (matchRegex, dec) => { + return String.fromCharCode(dec); + }); +} + +export function sanitizeUrl(url?: string): string { + if (!url) { + return BLANK_URL; + } + + const sanitizedUrl = decodeHtmlCharacters(url) + .replace(htmlCtrlEntityRegex, "") + .replace(ctrlCharactersRegex, "") + .trim(); + + if (!sanitizedUrl) { + return BLANK_URL; + } + + if (isRelativeUrlWithoutProtocol(sanitizedUrl)) { + return sanitizedUrl; + } + + const urlSchemeParseResults = sanitizedUrl.match(urlSchemeRegex); + + if (!urlSchemeParseResults) { + return sanitizedUrl; + } + + const urlScheme = urlSchemeParseResults[0]; + + if (invalidProtocolRegex.test(urlScheme)) { + return BLANK_URL; + } + + return sanitizedUrl; +} diff --git a/test/tests/util/index.js b/test/tests/util/index.js index 61858d9..19e0f71 100644 --- a/test/tests/util/index.js +++ b/test/tests/util/index.js @@ -11,3 +11,4 @@ import "./stringifyErrorMessage"; import "./isRegex"; import "./isDefined"; import "./base64encode"; +import "./sanitizeUrl"; diff --git a/test/tests/util/sanitizeUrl.js b/test/tests/util/sanitizeUrl.js new file mode 100644 index 0000000..0f1e207 --- /dev/null +++ b/test/tests/util/sanitizeUrl.js @@ -0,0 +1,31 @@ +/* @flow */ + +import { sanitizeUrl } from "../../../src/util"; + +describe("sanitizeUrl", () => { + it("should return about:blank for malicious URLs", () => { + const testURL = + 'https://www.paypal.com/smart/checkout/venmo/popup?parentDomain=www.paypal.com&venmoWebEnabled=true&venmoWebUrl=javascript:alert(document["cookie"])//'; + const sanitizedUrl = sanitizeUrl(testURL); + + if (sanitizedUrl !== "about:blank") { + throw new Error( + `Does not match. Original data:\n\n${testURL} + \n\nSanitized:\n\n${"about:blank"}\n` + ); + } + }); + + it("should return original URL when URL is clean", () => { + const testURL = + "https://www.paypal.com/smart/checkout/venmo/popup?parentDomain=www.paypal.com&venmoWebEnabled=true&venmoWebUrl=www.venmo.com"; + const sanitizedUrl = sanitizeUrl(testURL); + + if (sanitizedUrl !== testURL) { + throw new Error( + `Does not match. Original data:\n\n${testURL} + \n\nSanitized:\n\n${sanitizedUrl}\n` + ); + } + }); +});