mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
261 lines
8.9 KiB
261 lines
8.9 KiB
/* eslint-disable no-script-url */
|
|
import { sanitizeUrl } from "..";
|
|
import { BLANK_URL } from "../constants";
|
|
|
|
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)",
|
|
"java&NewLine&newline;;script:alert('XSS')",
|
|
"java&NewLine&newline;;script:alert('XSS')",
|
|
];
|
|
|
|
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')");
|
|
});
|
|
|
|
it("removes whitespace escape sequences", () => {
|
|
const attackVectors = [
|
|
"javascri\npt:alert('xss')",
|
|
"javascri\rpt:alert('xss')",
|
|
"javascri\tpt:alert('xss')",
|
|
"javascrip\\%74t:alert('XSS')",
|
|
"javascrip%5c%72t:alert()",
|
|
"javascrip%5Ctt:alert()",
|
|
"javascrip%255Ctt:alert()",
|
|
"javascrip%25%35Ctt:alert()",
|
|
"javascrip%25%35%43tt:alert()",
|
|
"javascrip%25%32%35%25%33%35%25%34%33rt:alert()",
|
|
"javascrip%255Crt:alert('%25xss')",
|
|
];
|
|
|
|
attackVectors.forEach((vector) => {
|
|
expect(sanitizeUrl(vector)).toBe(BLANK_URL);
|
|
});
|
|
});
|
|
|
|
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}`, () => {
|
|
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`
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|