/* eslint-disable no-script-url */ import { sanitizeUrl, BLANK_URL } from ".."; describe("sanitizeUrl", () => { it("does not alter http URLs with alphanumeric characters", () => { expect(sanitizeUrl("http://example.com/path/to:something")).toBe( "http://example.com/path/to:something" ); }); it("does not alter http URLs with ports with alphanumeric characters", () => { expect(sanitizeUrl("http://example.com:4567/path/to:something")).toBe( "http://example.com:4567/path/to:something" ); }); it("does not alter https URLs with alphanumeric characters", () => { expect(sanitizeUrl("https://example.com")).toBe("https://example.com"); }); it("does not alter https URLs with ports with alphanumeric characters", () => { expect(sanitizeUrl("https://example.com:4567/path/to:something")).toBe( "https://example.com:4567/path/to:something" ); }); it("does not alter relative-path reference URLs with alphanumeric characters", () => { expect(sanitizeUrl("./path/to/my.json")).toBe("./path/to/my.json"); }); it("does not alter absolute-path reference URLs with alphanumeric characters", () => { expect(sanitizeUrl("/path/to/my.json")).toBe("/path/to/my.json"); }); it("does not alter protocol-less network-path URLs with alphanumeric characters", () => { expect(sanitizeUrl("//google.com/robots.txt")).toBe( "//google.com/robots.txt" ); }); it("does not alter protocol-less URLs with alphanumeric characters", () => { expect(sanitizeUrl("www.example.com")).toBe("www.example.com"); }); it("does not alter deep-link urls with alphanumeric characters", () => { expect(sanitizeUrl("com.braintreepayments.demo://example")).toBe( "com.braintreepayments.demo://example" ); }); it("does not alter mailto urls with alphanumeric characters", () => { expect(sanitizeUrl("mailto:test@example.com?subject=hello+world")).toBe( "mailto:test@example.com?subject=hello+world" ); }); it("does not alter urls with accented characters", () => { expect(sanitizeUrl("www.example.com/with-áccêntš")).toBe( "www.example.com/with-áccêntš" ); }); it("does not strip harmless unicode characters", () => { expect(sanitizeUrl("www.example.com/лот.рфшишкиü–")).toBe( "www.example.com/лот.рфшишкиü–" ); }); it("strips out ctrl chars", () => { expect( sanitizeUrl("www.example.com/\u200D\u0000\u001F\x00\x1F\uFEFFfoo") ).toBe("www.example.com/foo"); }); it(`replaces blank urls with ${BLANK_URL}`, () => { expect(sanitizeUrl("")).toBe(BLANK_URL); }); it(`replaces null values with ${BLANK_URL}`, () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore expect(sanitizeUrl(null)).toBe(BLANK_URL); }); it(`replaces undefined values with ${BLANK_URL}`, () => { expect(sanitizeUrl()).toBe(BLANK_URL); }); it("removes whitespace from urls", () => { expect(sanitizeUrl(" http://example.com/path/to:something ")).toBe( "http://example.com/path/to:something" ); }); it("removes newline entities from urls", () => { expect(sanitizeUrl("https://example.com /something")).toBe( "https://example.com/something" ); }); it("decodes html entities", () => { // all these decode to javascript:alert('xss'); const attackVectors = [ "javascript:alert('XSS')", "javascript:alert('XSS')", "javascript:alert('XSS')", "jav ascript:alert('XSS');", "  javascript:alert('XSS');", "javasc ript: alert('XSS');", "javasc&#\u0000x09;ript:alert(1)", ]; attackVectors.forEach((vector) => { expect(sanitizeUrl(vector)).toBe(BLANK_URL); }); // https://example.com/javascript:alert('XSS') // since the javascript is the url path, and not the protocol, // this url is technically sanitized expect( sanitizeUrl( "https://example.com/javascript:alert('XSS')" ) ).toBe("https://example.com/javascript:alert('XSS')"); }); describe("invalid protocols", () => { describe.each(["javascript", "data", "vbscript"])("%s", (protocol) => { it(`replaces ${protocol} urls with ${BLANK_URL}`, () => { expect(sanitizeUrl(`${protocol}:alert(document.domain)`)).toBe( BLANK_URL ); }); it(`allows ${protocol} urls that start with a letter prefix`, () => { expect(sanitizeUrl(`not_${protocol}:alert(document.domain)`)).toBe( `not_${protocol}:alert(document.domain)` ); }); it(`disallows ${protocol} urls that start with non-\w characters as a suffix for the protocol`, () => { expect(sanitizeUrl(`&!*${protocol}:alert(document.domain)`)).toBe( BLANK_URL ); }); it(`disallows ${protocol} urls that use : for the colon portion of the url`, () => { expect(sanitizeUrl(`${protocol}:alert(document.domain)`)).toBe( BLANK_URL ); expect(sanitizeUrl(`${protocol}:alert(document.domain)`)).toBe( BLANK_URL ); }); it(`disregards capitalization for ${protocol} urls`, () => { // upper case every other letter in protocol name const mixedCapitalizationProtocol = protocol .split("") .map((character, index) => { if (index % 2 === 0) { return character.toUpperCase(); } return character; }) .join(""); expect( sanitizeUrl(`${mixedCapitalizationProtocol}:alert(document.domain)`) ).toBe(BLANK_URL); }); it(`ignores invisible ctrl characters in ${protocol} urls`, () => { const protocolWithControlCharacters = protocol .split("") .map((character, index) => { if (index === 1) { return character + "%EF%BB%BF%EF%BB%BF"; } else if (index === 2) { return character + "%e2%80%8b"; } return character; }) .join(""); expect( sanitizeUrl( decodeURIComponent( `${protocolWithControlCharacters}:alert(document.domain)` ) ) ).toBe(BLANK_URL); }); it(`replaces ${protocol} urls with ${BLANK_URL} when url begins with %20`, () => { expect( sanitizeUrl( decodeURIComponent(`%20%20%20%20${protocol}:alert(document.domain)`) ) ).toBe(BLANK_URL); }); it(`replaces ${protocol} urls with ${BLANK_URL} when ${protocol} url begins with spaces`, () => { expect(sanitizeUrl(` ${protocol}:alert(document.domain)`)).toBe( BLANK_URL ); }); it(`does not replace ${protocol}: if it is not in the scheme of the URL`, () => { expect(sanitizeUrl(`http://example.com#${protocol}:foo`)).toBe( `http://example.com#${protocol}:foo` ); }); }); }); });