diff --git a/packages/excalidraw/data/library.test.ts b/packages/excalidraw/data/library.test.ts new file mode 100644 index 000000000..bbbfd5f22 --- /dev/null +++ b/packages/excalidraw/data/library.test.ts @@ -0,0 +1,105 @@ +import { validateLibraryUrl } from "./library"; + +describe("validateLibraryUrl", () => { + it("should validate hostname & pathname", () => { + // valid hostnames + // ------------------------------------------------------------------------- + expect( + validateLibraryUrl("https://www.excalidraw.com", ["excalidraw.com"]), + ).toBe(true); + expect( + validateLibraryUrl("https://excalidraw.com", ["excalidraw.com"]), + ).toBe(true); + expect( + validateLibraryUrl("https://library.excalidraw.com", ["excalidraw.com"]), + ).toBe(true); + expect( + validateLibraryUrl("https://library.excalidraw.com", [ + "library.excalidraw.com", + ]), + ).toBe(true); + expect( + validateLibraryUrl("https://excalidraw.com/", ["excalidraw.com/"]), + ).toBe(true); + expect( + validateLibraryUrl("https://excalidraw.com", ["excalidraw.com/"]), + ).toBe(true); + expect( + validateLibraryUrl("https://excalidraw.com/", ["excalidraw.com"]), + ).toBe(true); + + // valid pathnames + // ------------------------------------------------------------------------- + expect( + validateLibraryUrl("https://excalidraw.com/path", ["excalidraw.com"]), + ).toBe(true); + expect( + validateLibraryUrl("https://excalidraw.com/path/", ["excalidraw.com"]), + ).toBe(true); + expect( + validateLibraryUrl("https://excalidraw.com/specific/path", [ + "excalidraw.com/specific/path", + ]), + ).toBe(true); + expect( + validateLibraryUrl("https://excalidraw.com/specific/path/", [ + "excalidraw.com/specific/path", + ]), + ).toBe(true); + expect( + validateLibraryUrl("https://excalidraw.com/specific/path", [ + "excalidraw.com/specific/path/", + ]), + ).toBe(true); + expect( + validateLibraryUrl("https://excalidraw.com/specific/path/other", [ + "excalidraw.com/specific/path", + ]), + ).toBe(true); + + // invalid hostnames + // ------------------------------------------------------------------------- + expect(() => + validateLibraryUrl("https://xexcalidraw.com", ["excalidraw.com"]), + ).toThrow(); + expect(() => + validateLibraryUrl("https://x-excalidraw.com", ["excalidraw.com"]), + ).toThrow(); + expect(() => + validateLibraryUrl("https://excalidraw.comx", ["excalidraw.com"]), + ).toThrow(); + expect(() => + validateLibraryUrl("https://excalidraw.comx", ["excalidraw.com"]), + ).toThrow(); + expect(() => + validateLibraryUrl("https://excalidraw.com.mx", ["excalidraw.com"]), + ).toThrow(); + // protocol must be https + expect(() => + validateLibraryUrl("http://excalidraw.com.mx", ["excalidraw.com"]), + ).toThrow(); + + // invalid pathnames + // ------------------------------------------------------------------------- + expect(() => + validateLibraryUrl("https://excalidraw.com/specific/other/path", [ + "excalidraw.com/specific/path", + ]), + ).toThrow(); + expect(() => + validateLibraryUrl("https://excalidraw.com/specific/paths", [ + "excalidraw.com/specific/path", + ]), + ).toThrow(); + expect(() => + validateLibraryUrl("https://excalidraw.com/specific/path-s", [ + "excalidraw.com/specific/path", + ]), + ).toThrow(); + expect(() => + validateLibraryUrl("https://excalidraw.com/some/specific/path", [ + "excalidraw.com/specific/path", + ]), + ).toThrow(); + }); +}); diff --git a/packages/excalidraw/data/library.ts b/packages/excalidraw/data/library.ts index 7eb760c64..1c23edcf0 100644 --- a/packages/excalidraw/data/library.ts +++ b/packages/excalidraw/data/library.ts @@ -36,7 +36,18 @@ import { Queue } from "../queue"; import { hashElementsVersion, hashString } from "../element"; import { toValidURL } from "./url"; -const ALLOWED_LIBRARY_HOSTNAMES = ["excalidraw.com"]; +/** + * format: hostname or hostname/pathname + * + * Both hostname and pathname are matched partially, + * hostname from the end, pathname from the start, with subdomain/path + * boundaries + **/ +const ALLOWED_LIBRARY_URLS = [ + "excalidraw.com", + // when installing from github PRs + "raw.githubusercontent.com/excalidraw/excalidraw-libraries", +]; type LibraryUpdate = { /** deleted library items since last onLibraryChange event */ @@ -469,26 +480,37 @@ export const distributeLibraryItemsOnSquareGrid = ( return resElements; }; -const validateLibraryUrl = ( +export const validateLibraryUrl = ( libraryUrl: string, /** - * If supplied, takes precedence over the default whitelist. - * Return `true` if the URL is valid. + * @returns `true` if the URL is valid, throws otherwise. */ - validator?: (libraryUrl: string) => boolean, -): boolean => { + validator: + | ((libraryUrl: string) => boolean) + | string[] = ALLOWED_LIBRARY_URLS, +): true => { if ( - validator + typeof validator === "function" ? validator(libraryUrl) - : ALLOWED_LIBRARY_HOSTNAMES.includes( - new URL(libraryUrl).hostname.split(".").slice(-2).join("."), - ) + : validator.some((allowedUrlDef) => { + const allowedUrl = new URL( + `https://${allowedUrlDef.replace(/^https?:\/\//, "")}`, + ); + + const { hostname, pathname } = new URL(libraryUrl); + + return ( + new RegExp(`(^|\\.)${allowedUrl.hostname}$`).test(hostname) && + new RegExp( + `^${allowedUrl.pathname.replace(/\/+$/, "")}(/+|$)`, + ).test(pathname) + ); + }) ) { return true; } - console.error(`Invalid or disallowed library URL: "${libraryUrl}"`); - throw new Error("Invalid or disallowed library URL"); + throw new Error(`Invalid or disallowed library URL: "${libraryUrl}"`); }; export const parseLibraryTokensFromUrl = () => {